diff --git a/common/test_utils.go b/common/test_utils.go index a76022f5..9efb9485 100644 --- a/common/test_utils.go +++ b/common/test_utils.go @@ -250,12 +250,45 @@ func DisplayEthTxs(txs map[string]ethtypes.Transactions) { for domain, domainTxs := range txs { fmt.Printf("domain: %s\n", domain) for _, tx := range domainTxs { - fmt.Printf("tx: %s\n", tx.Hash().Hex()) + sender, err := ExtractSender(tx) + if err != nil { + panic(err) + } + fmt.Printf("sender: %s tx: %s:%d\n", sender, tx.Hash().Hex(), tx.Nonce()) } } fmt.Printf("========txs info end=======\n") } +func FindTxHash(txLhs *ethtypes.Transaction, txs []*ethtypes.Transaction) bool { + if txLhs == nil { + panic("txLhs is nil") + } + lhsHash := txLhs.Hash() + for _, tx := range txs { + rhsHash := tx.Hash() + if lhsHash == rhsHash { + return true + } + } + return false +} + +func TxsHashUnorderedMatch(txsLhs []*ethtypes.Transaction, txsRhs []*ethtypes.Transaction) bool { + if len(txsLhs) != len(txsRhs) { + return false + } + + for _, tx := range txsLhs { + if !FindTxHash(tx, txsRhs) { + fmt.Printf("could not find tx hash " + tx.Hash().Hex()) + return false + } + } + + return true +} + func SyncMapLen(m *sync.Map) int { var length int m.Range(func(_, _ interface{}) bool { diff --git a/common/types.go b/common/types.go index 70f32475..10b2a77d 100644 --- a/common/types.go +++ b/common/types.go @@ -23,6 +23,7 @@ import ( "github.com/AnomalyFi/hypersdk/chain" "github.com/AnomalyFi/hypersdk/codec" + abls "github.com/AnomalyFi/hypersdk/crypto/bls" "github.com/AnomalyFi/hypersdk/utils" "github.com/AnomalyFi/nodekit-seq/actions" "github.com/ava-labs/avalanchego/ids" @@ -204,10 +205,12 @@ type ToBChunk struct { txHash2BundleHash map[string]string revertingTxHashes map[string]struct{} + // refers to whether bundle at idx has been removed removedBitSet *bitset.BitSet - domains map[string]struct{} // chain ids or rollup ids - seqTxs []*chain.Transaction - initialized bool + + domains map[string]struct{} // chain ids or rollup ids + seqTxs []*chain.Transaction + initialized bool l sync.RWMutex } @@ -228,6 +231,17 @@ func (tob *ToBChunk) GetBundles() []*CrossRollupBundle { return ret } +func (tob *ToBChunk) IsBundleIdxFiltered(idx int) bool { + tob.l.RLock() + defer tob.l.RUnlock() + + if idx >= len(tob.Bundles) { + return false + } + + return tob.removedBitSet.Test(uint(idx)) +} + func (tob *ToBChunk) GetTxs() map[string]ethtypes.Transactions { tob.l.RLock() defer tob.l.RUnlock() @@ -343,10 +357,45 @@ func (tob *ToBChunk) removeBundleContainTx(txHash string) (*CrossRollupBundle, e bundleIdx2remove := slices.IndexFunc(tob.Bundles, func(crb *CrossRollupBundle) bool { return crb.BundleHash == bundleHash }) + // mark as removed tob.removedBitSet = tob.removedBitSet.Set(uint(bundleIdx2remove)) // re-populate [tob.txs] + tob.repopulateToBTxs() + + return tob.Bundles[bundleIdx2remove], nil +} + +func (tob *ToBChunk) FilterBundleWithHash(bundleHash string) (*CrossRollupBundle, error) { + tob.l.Lock() + defer tob.l.Unlock() + + var foundIdx int + var foundBundle *CrossRollupBundle + + for bundleIdx, bundle := range tob.Bundles { + if bundle.BundleHash == bundleHash { + foundIdx = bundleIdx + foundBundle = bundle + break + } + } + + if foundBundle == nil { + return nil, fmt.Errorf("filterBundleWithHash found no bundle hash [%s]", bundleHash) + } + + // mark as removed + tob.removedBitSet = tob.removedBitSet.Set(uint(foundIdx)) + + // re-populate [tob.txs] + tob.repopulateToBTxs() + + return tob.Bundles[foundIdx], nil +} + +func (tob *ToBChunk) repopulateToBTxs() { tob.txs = make(map[string]ethtypes.Transactions) for bundleIdx, bundle := range tob.Bundles { // continue as this bundle was removed @@ -359,13 +408,12 @@ func (tob *ToBChunk) removeBundleContainTx(txHash string) (*CrossRollupBundle, e tob.txs[domain] = l } } + // track domains that contain txs in it tob.domains = make(map[string]struct{}) for domain := range tob.txs { tob.domains[domain] = struct{}{} } - - return tob.Bundles[bundleIdx2remove], nil } // LowestBlockNumber return the tracked lowest heights for domains, this prevents the situation that @@ -526,7 +574,7 @@ type RoBChunk struct { BlockNumber uint64 `json:"block_number"` // following fields will be populated after initialization - removedBitSet *bitset.BitSet + removedBitSet *bitset.BitSet // refers to whether tx within txs at idx has been removed initialized bool txs ethtypes.Transactions seqTxs []*chain.Transaction @@ -1135,6 +1183,23 @@ type GetBlockPayloadFromArcadia struct { BlockNumber uint64 `json:"blockNumber"` } +func (req *GetBlockPayloadFromArcadia) Payload() ([]byte, error) { + return json.Marshal(req) +} + +func (req *GetBlockPayloadFromArcadia) VerifyIssuer(networkID uint32, seqChainID ids.ID, pk *abls.PublicKey, sig *abls.Signature) (bool, error) { + payload, err := req.Payload() + if err != nil { + return false, err + } + uwm, err := warp.NewUnsignedMessage(networkID, seqChainID, payload) + if err != nil { + return false, err + } + uwmBytes := uwm.Bytes() + return abls.Verify(uwmBytes, pk, sig), nil +} + type SEQTxWrapper struct { Tx *chain.Transaction Size int @@ -1505,6 +1570,10 @@ func (b *CrossRollupBundle) Domains() []string { return maps.Keys(b.txs) } +func (b *CrossRollupBundle) HasTxs() bool { + return len(b.txs) > 0 +} + func (b *CrossRollupBundle) ContainTx(txHash string) bool { for _, txs := range b.txs { contain := slices.ContainsFunc(txs, func(t *ethtypes.Transaction) bool { diff --git a/common/utils.go b/common/utils.go index 323bb94e..a56b909f 100644 --- a/common/utils.go +++ b/common/utils.go @@ -12,6 +12,7 @@ import ( "strings" "time" + abls "github.com/AnomalyFi/hypersdk/crypto/bls" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/AnomalyFi/hypersdk/chain" @@ -221,3 +222,15 @@ func BuildDACert(chunk *ArcadiaChunk, epoch uint64, certificate []byte) *seqtype return robDA } } + +func HeaderToSEQSignature(sigHeader string) (*abls.Signature, error) { + sigBytes, err := hexutil.Decode(sigHeader) + if err != nil { + return nil, err + } + return abls.SignatureFromBytes(sigBytes) +} + +func BytesToSEQPubkey(pkBytes []byte) (*abls.PublicKey, error) { + return abls.PublicKeyFromBytes(pkBytes) +} diff --git a/database/mockdb.go b/database/mockdb.go index 70ea846b..d5fd71b3 100644 --- a/database/mockdb.go +++ b/database/mockdb.go @@ -29,7 +29,7 @@ type MockDB struct { RoBChunkMap map[string]map[uint64]*common.RoBChunkDB RoBChunkAcceptedMap map[string]*common.RoBChunkAcceptedDB LastFetchedBlockNum common.LastFetchedBlockNumberDB - PayloadResp common.PayloadDB + PayloadResp *common.PayloadDB Epoch uint64 EpochLowestToBNonce map[uint64]uint64 PayloadTxsToB map[string]*common.PayloadTxs @@ -54,7 +54,6 @@ func NewMockDB() *MockDB { RoBChunkMap: make(map[string]map[uint64]*common.RoBChunkDB), RoBChunkAcceptedMap: make(map[string]*common.RoBChunkAcceptedDB), LastFetchedBlockNum: common.LastFetchedBlockNumberDB{}, - PayloadResp: common.PayloadDB{}, Epoch: 0, EpochLowestToBNonce: make(map[uint64]uint64), PayloadTxsToB: make(map[string]*common.PayloadTxs), @@ -247,8 +246,10 @@ func (db *MockDB) RemoveBestAuctionBid(epoch uint64) error { func (db *MockDB) GetPayloadResp(chainID string, blockNumber uint64) (*common.PayloadDB, error) { db.l.Lock() defer db.l.Unlock() - - return &db.PayloadResp, nil + if db.PayloadResp == nil { + return nil, nil + } + return db.PayloadResp, nil } func (db *MockDB) SetPayloadResp(chainID string, blockNumber uint64, txs *common.GetPayloadResponse) error { @@ -260,7 +261,7 @@ func (db *MockDB) SetPayloadResp(chainID string, blockNumber uint64, txs *common return err } - db.PayloadResp = *payloadTxs + db.PayloadResp = payloadTxs return nil } diff --git a/datalayer/mocks/mock_IDASubmitter.go b/datalayer/mocks/mock_IDASubmitter.go index b83dcace..0f0ea340 100644 --- a/datalayer/mocks/mock_IDASubmitter.go +++ b/datalayer/mocks/mock_IDASubmitter.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.50.0. DO NOT EDIT. +// Code generated by mockery v2.39.0. DO NOT EDIT. package mocks diff --git a/datastore/mocks/mock_IDatastore.go b/datastore/mocks/mock_IDatastore.go index bb2c836b..01d011c3 100644 --- a/datastore/mocks/mock_IDatastore.go +++ b/datastore/mocks/mock_IDatastore.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.50.0. DO NOT EDIT. +// Code generated by mockery v2.39.0. DO NOT EDIT. package mocks @@ -372,7 +372,7 @@ func (_c *MockIDatastore_LoadChunksWithToBNonce_Call) RunAndReturn(run func(uint return _c } -// LoadCurrentToBNonce provides a mock function with no fields +// LoadCurrentToBNonce provides a mock function with given fields: func (_m *MockIDatastore) LoadCurrentToBNonce() (*uint64, error) { ret := _m.Called() @@ -429,7 +429,7 @@ func (_c *MockIDatastore_LoadCurrentToBNonce_Call) RunAndReturn(run func() (*uin return _c } -// LoadHighestSettledToBNonce provides a mock function with no fields +// LoadHighestSettledToBNonce provides a mock function with given fields: func (_m *MockIDatastore) LoadHighestSettledToBNonce() (*uint64, error) { ret := _m.Called() @@ -486,7 +486,7 @@ func (_c *MockIDatastore_LoadHighestSettledToBNonce_Call) RunAndReturn(run func( return _c } -// LoadLowestManagedStateToBNonce provides a mock function with no fields +// LoadLowestManagedStateToBNonce provides a mock function with given fields: func (_m *MockIDatastore) LoadLowestManagedStateToBNonce() (*uint64, error) { ret := _m.Called() diff --git a/seq/consts.go b/seq/consts.go new file mode 100644 index 00000000..2f0c7f48 --- /dev/null +++ b/seq/consts.go @@ -0,0 +1,4 @@ +package seq + +const DefaultProposerLRUSize = 20 +const GetArcadiaBlockSignatureHeader = "X-Arcadia-GetBlock-Sig" diff --git a/seq/mocks/mock_BaseSeqClient.go b/seq/mocks/mock_BaseSeqClient.go index 822354e2..eb932b09 100644 --- a/seq/mocks/mock_BaseSeqClient.go +++ b/seq/mocks/mock_BaseSeqClient.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.50.0. DO NOT EDIT. +// Code generated by mockery v2.39.0. DO NOT EDIT. package mocks @@ -30,7 +30,7 @@ func (_m *MockBaseSeqClient) EXPECT() *MockBaseSeqClient_Expecter { return &MockBaseSeqClient_Expecter{mock: &_m.Mock} } -// CurrentEpoch provides a mock function with no fields +// CurrentEpoch provides a mock function with given fields: func (_m *MockBaseSeqClient) CurrentEpoch() uint64 { ret := _m.Called() @@ -123,7 +123,7 @@ func (_c *MockBaseSeqClient_CurrentValidators_Call) RunAndReturn(run func(contex return _c } -// CurrentValidatorsTotalWeight provides a mock function with no fields +// CurrentValidatorsTotalWeight provides a mock function with given fields: func (_m *MockBaseSeqClient) CurrentValidatorsTotalWeight() uint64 { ret := _m.Called() @@ -284,7 +284,7 @@ func (_c *MockBaseSeqClient_GetBalance_Call) RunAndReturn(run func(context.Conte return _c } -// GetChainID provides a mock function with no fields +// GetChainID provides a mock function with given fields: func (_m *MockBaseSeqClient) GetChainID() ids.ID { ret := _m.Called() @@ -331,7 +331,7 @@ func (_c *MockBaseSeqClient_GetChainID_Call) RunAndReturn(run func() ids.ID) *Mo return _c } -// GetNetworkID provides a mock function with no fields +// GetNetworkID provides a mock function with given fields: func (_m *MockBaseSeqClient) GetNetworkID() uint32 { ret := _m.Called() @@ -481,7 +481,7 @@ func (_c *MockBaseSeqClient_GetValidatorWeight_Call) RunAndReturn(run func([]byt return _c } -// Parser provides a mock function with no fields +// Parser provides a mock function with given fields: func (_m *MockBaseSeqClient) Parser() chain.Parser { ret := _m.Called() @@ -528,7 +528,66 @@ func (_c *MockBaseSeqClient_Parser_Call) RunAndReturn(run func() chain.Parser) * return _c } -// SeqHead provides a mock function with no fields +// ProposerAtHeight provides a mock function with given fields: ctx, height +func (_m *MockBaseSeqClient) ProposerAtHeight(ctx context.Context, height uint64) (*rpc.Validator, error) { + ret := _m.Called(ctx, height) + + if len(ret) == 0 { + panic("no return value specified for ProposerAtHeight") + } + + var r0 *rpc.Validator + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, uint64) (*rpc.Validator, error)); ok { + return rf(ctx, height) + } + if rf, ok := ret.Get(0).(func(context.Context, uint64) *rpc.Validator); ok { + r0 = rf(ctx, height) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*rpc.Validator) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, uint64) error); ok { + r1 = rf(ctx, height) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockBaseSeqClient_ProposerAtHeight_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ProposerAtHeight' +type MockBaseSeqClient_ProposerAtHeight_Call struct { + *mock.Call +} + +// ProposerAtHeight is a helper method to define mock.On call +// - ctx context.Context +// - height uint64 +func (_e *MockBaseSeqClient_Expecter) ProposerAtHeight(ctx interface{}, height interface{}) *MockBaseSeqClient_ProposerAtHeight_Call { + return &MockBaseSeqClient_ProposerAtHeight_Call{Call: _e.mock.On("ProposerAtHeight", ctx, height)} +} + +func (_c *MockBaseSeqClient_ProposerAtHeight_Call) Run(run func(ctx context.Context, height uint64)) *MockBaseSeqClient_ProposerAtHeight_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(uint64)) + }) + return _c +} + +func (_c *MockBaseSeqClient_ProposerAtHeight_Call) Return(_a0 *rpc.Validator, _a1 error) *MockBaseSeqClient_ProposerAtHeight_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockBaseSeqClient_ProposerAtHeight_Call) RunAndReturn(run func(context.Context, uint64) (*rpc.Validator, error)) *MockBaseSeqClient_ProposerAtHeight_Call { + _c.Call.Return(run) + return _c +} + +// SeqHead provides a mock function with given fields: func (_m *MockBaseSeqClient) SeqHead() *chain.StatefulBlock { ret := _m.Called() @@ -604,11 +663,11 @@ func (_c *MockBaseSeqClient_SetOnNewBlockHandler_Call) Return() *MockBaseSeqClie } func (_c *MockBaseSeqClient_SetOnNewBlockHandler_Call) RunAndReturn(run func(func(*chain.StatefulBlock, []*chain.Result))) *MockBaseSeqClient_SetOnNewBlockHandler_Call { - _c.Run(run) + _c.Call.Return(run) return _c } -// Stop provides a mock function with no fields +// Stop provides a mock function with given fields: func (_m *MockBaseSeqClient) Stop() { _m.Called() } @@ -636,7 +695,7 @@ func (_c *MockBaseSeqClient_Stop_Call) Return() *MockBaseSeqClient_Stop_Call { } func (_c *MockBaseSeqClient_Stop_Call) RunAndReturn(run func()) *MockBaseSeqClient_Stop_Call { - _c.Run(run) + _c.Call.Return(run) return _c } diff --git a/seq/seqclient.go b/seq/seqclient.go index 8ec721f9..6266c582 100644 --- a/seq/seqclient.go +++ b/seq/seqclient.go @@ -20,6 +20,7 @@ import ( "github.com/AnomalyFi/nodekit-seq/auth" "github.com/AnomalyFi/nodekit-seq/consts" srpc "github.com/AnomalyFi/nodekit-seq/rpc" + avacache "github.com/ava-labs/avalanchego/cache" "github.com/ava-labs/avalanchego/ids" "github.com/sirupsen/logrus" ) @@ -55,6 +56,7 @@ type BaseSeqClient interface { SubmitActions(ctx context.Context, action []chain.Action) (ids.ID, error) GenerateTransaction(ctx context.Context, acts []chain.Action) (*chain.Transaction, error) + ProposerAtHeight(ctx context.Context, height uint64) (*hrpc.Validator, error) } var _ BaseSeqClient = (*SeqClient)(nil) @@ -69,6 +71,7 @@ type SeqClient struct { blockHead *chain.StatefulBlock blockHeadL sync.RWMutex + proposerAtHeight *avacache.LRU[uint64, *hrpc.Validator] currentValidators []*hrpc.Validator // ETH Chain related @@ -128,6 +131,9 @@ func NewSeqClient(config *SeqClientConfig) (*SeqClient, error) { wsCli: wsCli, signer: config.PrivateKey, + currentValidators: make([]*hrpc.Validator, 0), + proposerAtHeight: &avacache.LRU[uint64, *hrpc.Validator]{Size: DefaultProposerLRUSize}, + Namespace: nil, parser: parser, @@ -180,6 +186,19 @@ func NewSeqClient(config *SeqClientConfig) (*SeqClient, error) { client.logger.WithField("validatorPubkeys", validatorPubkeys).Debug("setting new validator set") } + // query the proposer at height + 1 + go func() { + proposer, err := client.hrpc.NextProposer(ctx, blk.Hght+1) + if err != nil { + client.logger.WithFields(logrus.Fields{ + "seqHeight": blk.Hght, + "err": err, + }).Warn("unable to query the next proposer at height") + return + } + client.proposerAtHeight.Put(blk.Hght+1, proposer) + }() + // calculate total weight of current validators. var totalWeight uint64 for _, val := range currVal { @@ -366,3 +385,18 @@ func (s *SeqClient) GetRollupsValidAtEpoch(ctx context.Context, epoch uint64) ([ } return seqRollups.Rollups, nil } + +func (s *SeqClient) ProposerAtHeight(ctx context.Context, height uint64) (*hrpc.Validator, error) { + proposer, exists := s.proposerAtHeight.Get(height) + if exists { + return proposer, nil + } + + proposer, err := s.hrpc.NextProposer(ctx, height) + if err != nil { + return nil, err + } + + s.proposerAtHeight.Put(height, proposer) + return proposer, nil +} diff --git a/services/api/service.go b/services/api/service.go index 5c4c8f1d..efee6edc 100644 --- a/services/api/service.go +++ b/services/api/service.go @@ -66,7 +66,6 @@ const ( CancelTimeoutSecs = 2 SlotWindowToCheckAuctionWinner = 2 SafetyNonceDifference = uint64(20) - MaxDARetries = int(5) ) // prometheus counters @@ -294,6 +293,9 @@ type ArcadiaAPI struct { // map of ChunkID -> PreConfInfo pendingChunksPreConfs sync.Map + // map of ChainID -> num messages received + numChunksByChainID sync.Map + // map of rollup id to block number. this is updated whenever rollup successfully calls getPayload(). // we have an issue in which builders cannot build for the block number that has been called via getPayload() // else we experience state corruption. This map is used to reject @@ -312,6 +314,7 @@ type ArcadiaAPI struct { // seq validator registration upgrader *websocket.Upgrader + // map of subscribed validators, on subscribe, we query its weight and store the connection and weight by [SubscribedValidator] // on disconnected, we remove that validator from the map subscribedValidatorMap sync.Map @@ -387,6 +390,7 @@ func NewArcadiaAPI(opts ArcadiaAPIOpts) (api *ArcadiaAPI, err error) { blockSimRateLimiter: opts.BlockSimulator, pendingChunksPreConfs: sync.Map{}, + numChunksByChainID: sync.Map{}, rollupToLastFetchedBlockNumber: make(map[string]uint64), submitReqOngoing: &sync.Map{}, @@ -436,6 +440,16 @@ func NewArcadiaAPI(opts ArcadiaAPIOpts) (api *ArcadiaAPI, err error) { return api, nil } +// setBlockSimRateLimiter used to set a block sim rate limiter. Only for test use. +func (api *ArcadiaAPI) setBlockSimRateLimiter(limiter simulator.IBlockSimRateLimiter) { + api.blockSimRateLimiter = limiter +} + +// setSeqClient used to set seq client on ArcadiaAPI. Only for test use. +func (api *ArcadiaAPI) setSeqClient(client seq.BaseSeqClient) { + api.seqClient = client +} + func (api *ArcadiaAPI) getRouter() http.Handler { // Main router mainRouter := mux.NewRouter() @@ -486,6 +500,26 @@ func (api *ArcadiaAPI) getRouter() http.Handler { return loggedRouter } +func (api *ArcadiaAPI) trackChunkByChainID(chainID string) { + numChunk, ok := api.numChunksByChainID.Load(chainID) + if !ok { + api.numChunksByChainID.Store(chainID, 1) + } else { + numChunkVal := numChunk.(int) + api.numChunksByChainID.Store(chainID, numChunkVal+1) + } +} + +func (api *ArcadiaAPI) getNumChunksByChainID(chainID string) int { + numChunk, ok := api.numChunksByChainID.Load(chainID) + if !ok { + return 0 + } else { + numChunkVal := numChunk.(int) + return numChunkVal + } +} + // StartServer starts up this API instance and HTTP server // - First it initializes the cache and updates local information // - Once that is done, the HTTP server is started @@ -1064,7 +1098,7 @@ func (api *ArcadiaAPI) handleGetPayload(w http.ResponseWriter, req *http.Request payload := new(common.GetPayloadRequest) if err := json.Unmarshal(body, payload); err != nil { log.WithError(err).Warn("failed to decode getPayload request") - api.RespondError(w, http.StatusBadRequest, "failed to decode anchor payload request") + api.RespondError(w, http.StatusBadRequest, "failed to decode payload request") return } log = log.WithFields(logrus.Fields{ @@ -1127,7 +1161,7 @@ func (api *ArcadiaAPI) handleGetPayload(w http.ResponseWriter, req *http.Request // try fetch cached payload var getPayloadResp *common.GetPayloadResponse getPayloadResp, err = api.redis.GetPayloadResp(payload.ChainID, payload.BlockNumber) - if err != nil { + if err != nil || getPayloadResp == nil { log.WithError(err).Warn("unable to get payload from redis, trying database...") payloadDB, err := api.db.GetPayloadResp(payload.ChainID, payload.BlockNumber) if err != nil { @@ -1140,6 +1174,7 @@ func (api *ArcadiaAPI) handleGetPayload(w http.ResponseWriter, req *http.Request log.WithError(err).Warn("unable to convert payload from database, conversion failed.") } } + if getPayloadResp != nil { api.RespondOK(w, getPayloadResp) log.Infof("execution payload(from cache) delivered, timestampAfterLoadResponse %d", time.Now().UTC().UnixMilli()) @@ -1609,7 +1644,15 @@ func (api *ArcadiaAPI) handleSubmitNewBlockRequest(w http.ResponseWriter, req *h // specific checks for either ToB or RoB if isToB { + bundlesToFilter := make([]*common.CrossRollupBundle, 0) for _, bundle := range blockReq.ToBChunk().Bundles { + if !bundle.HasTxs() { + errMsg := fmt.Sprintf("bundle with hash [%s] contained no txs, rejecting request", bundle.BundleHash) + log.WithError(err).Warn(errMsg) + api.RespondError(w, http.StatusBadRequest, errMsg) + return + } + for _, chainIDStr := range bundle.Domains() { namespace, err := common.ChainIDStrToNamespace(chainIDStr) if err != nil { @@ -1618,14 +1661,35 @@ func (api *ArcadiaAPI) handleSubmitNewBlockRequest(w http.ResponseWriter, req *h api.RespondError(w, http.StatusBadRequest, errMsg) return } + + // The given rollup for chain id must be registered for this auction period else we reject if !api.datastore.IsRollupRegistered(headEpoch, namespace) { errMsg := fmt.Sprintf("builder chunk tried to build tob chunk with cross bundle rollup for unregistered rollup chain id [%s]", chainIDStr) log.Warn(errMsg) api.RespondError(w, http.StatusBadRequest, errMsg) return } + + // We will filter out the ToB bundle if we haven't received a RoB for each chain id it is using + if api.getNumChunksByChainID(chainIDStr) == 0 { + warnMsg := fmt.Sprintf("tob bundle filtered out due to lack of RoB for chain ID [%s] ", chainIDStr) + api.log.Warn(warnMsg) + bundlesToFilter = append(bundlesToFilter, bundle) + } + } + + // mark any bundles to be filtered as removed + for _, bundle := range bundlesToFilter { + _, err = blockReq.ToBChunk().FilterBundleWithHash(bundle.BundleHash) + if err != nil { + errMsg := fmt.Sprintf("failed to find bundle hash [%s] when trying to filter, possible state corruption", bundle.BundleHash) + log.Warn(errMsg) + api.RespondError(w, http.StatusBadRequest, errMsg) + return + } } } + // query the latest block numbers for each domain and assign to ToB blockNumbers, err := api.blockSimRateLimiter.GetBlockNumber(blockReq.ToBChunk().Domains()) if err != nil { @@ -1765,6 +1829,15 @@ func (api *ArcadiaAPI) handleSubmitNewBlockRequest(w http.ResponseWriter, req *h "tobNonce": headToBNonce, }).Debug("incoming chunk txs") + // tracks the kind of chunk we are receiving + if isToB { + api.trackChunkByChainID("tob") + } else { + api.trackChunkByChainID(chainID) + } + + // Simulation handling + // At this point, filter checks have completed and we want to perform simulation on txs in the chunk var simulationErr error // label all successful bundles to be accepted defer func() { @@ -1794,6 +1867,21 @@ func (api *ArcadiaAPI) handleSubmitNewBlockRequest(w http.ResponseWriter, req *h return } + if isToB { + // If all bundles are filtered, then we can reject this block. + // Note that simulation can also filter blocks. + var foundValidBundle bool + for i := 0; i < len(blockReq.ToBChunk().Bundles); i++ { + foundValidBundle = foundValidBundle || !blockReq.ToBChunk().IsBundleIdxFiltered(i) + } + if !foundValidBundle { + warnMsg := "no bundles found in tob, all bundles filtered" + log.Warn(warnMsg) + api.RespondError(w, http.StatusNoContent, warnMsg) + return + } + } + // we will only know the stable ToBNonce after adding this chunk to chunk manager and chunk manager will assign the latest one to it if err := api.chunkManager.AddChunk(chunk); err != nil { log.WithError(err).Warn("unable to add chunk to chunk manager") @@ -2327,22 +2415,68 @@ func (api *ArcadiaAPI) handleGetArcadiaBlock(w http.ResponseWriter, req *http.Re body, err := io.ReadAll(req.Body) if err != nil { if strings.Contains(err.Error(), "i/o timeout") { - api.log.WithError(err).Error("handleGetArcadiaBlock() request failed to decode (i/o timeout)") + log.WithError(err).Error("handleGetArcadiaBlock() request failed to decode (i/o timeout)") api.RespondError(w, http.StatusInternalServerError, err.Error()) return } - api.log.Error("could not read body of request for handleGetArcadiaBlock()") + log.Error("could not read body of request for handleGetArcadiaBlock()") api.RespondError(w, http.StatusBadRequest, err.Error()) return } plreq := new(common.GetBlockPayloadFromArcadia) if err := json.NewDecoder(bytes.NewReader(body)).Decode(plreq); err != nil { - api.log.Error("failed to decode arcadia block request") + log.Error("failed to decode arcadia block request") + api.RespondError(w, http.StatusBadRequest, err.Error()) + return + } + + log = log.WithField("seqHeight", plreq.BlockNumber) + + // do proposer verification + ctx := context.Background() + ctx, cancel := context.WithTimeout(ctx, 500*time.Second) + defer cancel() + proposer, err := api.seqClient.ProposerAtHeight(ctx, plreq.BlockNumber) + if err != nil { + log.WithError(err).Error("failed to fetch proposer at height") + api.RespondError(w, http.StatusBadRequest, err.Error()) + return + } + proposerPubkey, err := common.BytesToSEQPubkey(proposer.PublicKey) + if err != nil { + log.WithError(err).Error("failed to parse proposer pubkey") api.RespondError(w, http.StatusBadRequest, err.Error()) return } + proposerSigStr := req.Header.Get(seq.GetArcadiaBlockSignatureHeader) + proposerSig, err := common.HeaderToSEQSignature(proposerSigStr) + if err != nil { + log.WithError(err).Error("failed to extract proposer signature from header") + api.RespondError(w, http.StatusBadRequest, err.Error()) + return + } + seqNetworkID := api.seqClient.GetNetworkID() + seqChainID := api.seqClient.GetChainID() + log = log.WithFields(logrus.Fields{ + "proposerPubkey": hexutil.Encode(proposer.PublicKey), + "sig": proposerSigStr, + "seqNetworkID": seqNetworkID, + "seqChainID": seqChainID.String(), + }) + verified, err := plreq.VerifyIssuer(seqNetworkID, seqChainID, proposerPubkey, proposerSig) + if err != nil { + log.WithError(err).Error("failed to verify signature of the get block request") + api.RespondError(w, http.StatusBadRequest, err.Error()) + return + } + if !verified { + log.Error("wrong sigature against the given payload") + api.RespondError(w, http.StatusBadRequest, "unable to verify signature") + return + } + txsw := api.pendingTxs.StreamTxs(int(plreq.MaxBandwidth)) var txs []*chain.Transaction for _, tx := range txsw { @@ -2361,13 +2495,13 @@ func (api *ArcadiaAPI) handleGetArcadiaBlock(w http.ResponseWriter, req *http.Re } } if len(txs) == 0 { - api.log.Errorf("no transactions available, block: %d", plreq.BlockNumber) + log.Errorf("no transactions available, block: %d", plreq.BlockNumber) api.RespondError(w, http.StatusNoContent, "no transactions available") return } mtxs, err := chain.MarshalTxs(txs) if err != nil { - api.log.Errorf("no transactions available, block: %d", plreq.BlockNumber) + log.Errorf("no transactions available, block: %d", plreq.BlockNumber) api.RespondError(w, http.StatusNoContent, "no transactions available") return } @@ -2391,13 +2525,12 @@ func (api *ArcadiaAPI) handleSubscribeValidator(w http.ResponseWriter, req *http epoch := api.headEpoch.Load() - log := api.log.WithField("method", "subscribeValidator") - log.Info("Received subscribe validator request") - log = api.log.WithFields(logrus.Fields{ + log := api.log.WithFields(logrus.Fields{ "method": "subscribeValidator", "contentLength": req.ContentLength, "timestampRequestStart": time.Now().UTC().UnixMilli(), }) + log.Info("Received subscribe validator request") conn, err := api.upgrader.Upgrade(w, req, nil) if err != nil { diff --git a/services/api/service_test.go b/services/api/service_test.go index 56d06ea1..0b3edbcd 100644 --- a/services/api/service_test.go +++ b/services/api/service_test.go @@ -8,19 +8,21 @@ import ( "encoding/hex" "encoding/json" "fmt" - "github.com/AnomalyFi/Arcadia/datalayer" - mda "github.com/AnomalyFi/Arcadia/datalayer/mocks" "io" "math/big" mrand "math/rand" "net/http" "net/http/httptest" + "slices" "strconv" "strings" "sync" "testing" "time" + "github.com/AnomalyFi/Arcadia/datalayer" + mda "github.com/AnomalyFi/Arcadia/datalayer/mocks" + "golang.org/x/exp/maps" "github.com/alicebob/miniredis/v2" @@ -40,6 +42,7 @@ import ( "github.com/AnomalyFi/Arcadia/common" "github.com/AnomalyFi/Arcadia/database" "github.com/AnomalyFi/Arcadia/datastore" + "github.com/AnomalyFi/Arcadia/seq" mseq "github.com/AnomalyFi/Arcadia/seq/mocks" "github.com/AnomalyFi/Arcadia/simulator" msim "github.com/AnomalyFi/Arcadia/simulator/mocks" @@ -68,8 +71,29 @@ var ( mockPublicKey, _ = bls.PublicKeyFromSecretKey(mockSecretKey) numTestAccounts = 10 testAccounts = GenerateTestEthAccounts(numTestAccounts) + + testOriginChainID = big.NewInt(45200) + testOriginChainIDStr = hexutil.EncodeBig(testOriginChainID) + testRemoteChainID = big.NewInt(45201) + testRemoteChainIDStr = hexutil.EncodeBig(testRemoteChainID) + testBlockNumbers = map[string]uint64{ + testOriginChainIDStr: 100, + testRemoteChainIDStr: 50, + } + testOriginChainIDInt, _ = common.ChainIDStrToChainID(testOriginChainIDStr) + testRemoteChainIDInt, _ = common.ChainIDStrToChainID(testRemoteChainIDStr) + testSeqChainID = ids.GenerateTestID() + testEpoch = uint64(0) + + testBuilderSecretKey, _ = bls.GenerateRandomSecretKey() + testBuilderPublicKey, _ = bls.PublicKeyFromSecretKey(testBuilderSecretKey) + testChainParser = &srpc.Parser{} ) +func init() { + _, _ = testChainParser.Registry() +} + type testBackend struct { t require.TestingT arcadia *ArcadiaAPI @@ -178,6 +202,65 @@ func newTestbackendWithCustomDatastore(t *testing.T, redisCache *datastore.Redis return &backend } +// resetMockExpectations is used to reset seqclient mock expectations +func (be *testBackend) resetSeqClientMockExpectations(t *testing.T) { + be.seqcli.AssertExpectations(t) + + be.seqcli = mseq.NewMockBaseSeqClient(t) + be.seqcli.EXPECT().SetOnNewBlockHandler(mock.Anything).Return().Maybe() + be.seqcli.EXPECT().Parser().Return(&srpc.Parser{}).Maybe() + + be.simulator = msim.NewMockIBlockSimRateLimiter(t) + be.arcadia.setBlockSimRateLimiter(be.simulator) + be.arcadia.setSeqClient(be.seqcli) +} + +// setupRoBsForToBTest is a helper which sends two RoB blocks for origin and remote chains. +// This primes +func (be *testBackend) setupRoBsForToBTest(t *testing.T) (*common.SubmitNewBlockRequest, *common.SubmitNewBlockRequest) { + be.SetupRegisteredRollups(testEpoch, testOriginChainID) + be.SetupRegisteredRollups(testEpoch, testRemoteChainID) + + // send in rob chunk for origin chain id + // needed for ToB to be accepted + robChainID := testOriginChainIDInt + robOpts1 := &CreateTestBlockSubmissionOpts{ + Epoch: testEpoch, + OriginChainID: *robChainID, + RemoteChainID: *testRemoteChainIDInt, + SeqChainID: testSeqChainID, + BuilderPubkey: *testBuilderPublicKey, + BuilderSecretkey: *testBuilderSecretKey, + RoBBlockNumber: testBlockNumbers[testOriginChainIDStr] + 1, + RoBChainID: robChainID, + ChunkID: ids.GenerateTestID(), + Txs: nil, + IsToB: false, + } + rob1 := be.submitRoBChunk(t, robOpts1, testChainParser) + + // send in rob chunk for remote chain id + // needed for ToB to be accepted + robChainID = testRemoteChainIDInt + robOpts2 := &CreateTestBlockSubmissionOpts{ + Epoch: testEpoch, + OriginChainID: *robChainID, + RemoteChainID: *testRemoteChainIDInt, + SeqChainID: testSeqChainID, + BuilderPubkey: *testBuilderPublicKey, + BuilderSecretkey: *testBuilderSecretKey, + RoBBlockNumber: testBlockNumbers[testRemoteChainIDStr] + 1, + RoBChainID: robChainID, + ChunkID: ids.GenerateTestID(), + Txs: nil, + IsToB: false, + } + rob2 := be.submitRoBChunk(t, robOpts2, testChainParser) + + be.resetSeqClientMockExpectations(t) + return rob1, rob2 +} + func newDatastores(t *testing.T) (*datastore.RedisCache, database.IDatabaseService) { redisClient, err := miniredis.Run() require.NoError(t, err) @@ -190,6 +273,105 @@ func newDatastores(t *testing.T) (*datastore.RedisCache, database.IDatabaseServi return redisCache, db } +// submitRoBChunk is a helper for submitting an rob chunk to the backend +// Note it will use the opts origin chain id for the chain and opts rob chain id must equal origin chain id +func (be *testBackend) submitRoBChunk(t *testing.T, opts *CreateTestBlockSubmissionOpts, parser *srpc.Parser) *common.SubmitNewBlockRequest { + if opts.RoBChainID == nil { + panic("robchain id is required") + } + + if opts.OriginChainID.Uint64() != opts.RoBChainID.Uint64() { + panic("opts origin chain id must equal rob chain id") + } + + builderPK := opts.BuilderPubkey + builderPKBytes := builderPK.Bytes() + originChainIDStr := common.ChainIDStr(&opts.OriginChainID) + redis := be.redis + redis.SetSizeTracker(be.arcadia.sizeTracker) + + // constructing RoB with txs from origin chain id + if opts.Txs == nil { + robTxs := ethtypes.Transactions{ + // for every first RoB for one chain, testAccounts[0] is used and the tx nonce used is 0 + CreateEthTransfer(t, &opts.OriginChainID, testAccounts[0].PrivateKey, testAccounts[1].Address, 20, testAccounts[0].Nonce, 21000, nil), + } + opts.Txs = robTxs + } + robReq := CreateRoBReq(t, opts) + + // builder needs to be the auction winner in order to build + err := be.arcadia.datastore.SetAuctionWinner(opts.Epoch, builderPKBytes[:]) + require.NoError(t, err) + + // register test rollup for the origin chain id so we can accept builder bids + be.SetupRegisteredRollups(opts.Epoch, &opts.OriginChainID) + + // set up chunk simulation mock expectations + be.seqcli.EXPECT().Parser().Return(parser) + + t.Log("========setup & send RoB============") + for domain, expectedTxs := range map[string]ethtypes.Transactions{ + originChainIDStr: opts.Txs, + } { + relatedAccounts, err := CollectAccountsFromTxs(expectedTxs, testAccounts) + require.NoError(t, err) + nonces := CollectNoncesFromEthAccounts(relatedAccounts) + balances := CollectBalancesFromEthAccounts(relatedAccounts) + + matchTxs := ExpectedMatchTxs(expectedTxs) + be.simulator.EXPECT().GetNonces(domain, mock.MatchedBy(matchTxs), uint64ToHexString(opts.RoBBlockNumber-1)).Return(nonces, nil) + be.simulator.EXPECT().GetBalances(domain, mock.MatchedBy(matchTxs), uint64ToHexString(opts.RoBBlockNumber-1)).Return(balances, nil) + + // simulation results, only the txs from this RoB will be simulated and the txs from ToB will be filtered out + callBundleRes := make([]flashbotsrpc.FlashbotsCallBundleResult, 0, len(expectedTxs)) + for _, tx := range expectedTxs { + callBundleRes = append(callBundleRes, flashbotsrpc.FlashbotsCallBundleResult{ + TxHash: tx.Hash().Hex(), + Error: "", + Revert: "", + }) + } + rawExpectedTxs, err := CollectRawTxs(expectedTxs) + require.NoError(t, err) + validationReq := common.BlockValidationRequest{ + Txs: rawExpectedTxs, + BlockNumber: uint64ToHexString(opts.RoBBlockNumber - 1), + StateBlockNumber: uint64ToHexString(opts.RoBBlockNumber - 1), + } + be.simulator.EXPECT(). + SimBlockAndGetGasUsedForChain(mock.Anything, domain, &validationReq). + Return(100, callBundleRes, nil) + } + + rr := be.request(http.MethodPost, pathSubmitNewBlockRequest, robReq) + require.Equal(t, http.StatusOK, rr.Code) + + return robReq +} + +func (be *testBackend) setupSimBlockAndGetUsedExpectation(t *testing.T, expectedTxs ethtypes.Transactions, blockNum uint64, domain string) { + // simulation results, only the txs from this ToB will be simulated and the txs from RoB will be filtered out + callBundleRes := make([]flashbotsrpc.FlashbotsCallBundleResult, 0, len(expectedTxs)) + for _, tx := range expectedTxs { + callBundleRes = append(callBundleRes, flashbotsrpc.FlashbotsCallBundleResult{ + TxHash: tx.Hash().Hex(), + Error: "", + Revert: "", + }) + } + rawExpectedTxs, err := CollectRawTxs(expectedTxs) + require.NoError(t, err) + validationReq := common.BlockValidationRequest{ + Txs: rawExpectedTxs, + BlockNumber: uint64ToHexString(blockNum), + StateBlockNumber: uint64ToHexString(blockNum), + } + be.simulator.EXPECT(). + SimBlockAndGetGasUsedForChain(mock.Anything, domain, &validationReq). + Return(100, callBundleRes, nil) +} + // setLowestToBNonceForEpoch is used to set the lowest tob nonce for epoch in datastore for testing // fails with error only if both redis and database error out func (be *testBackend) setLowestToBNonceForEpoch(t *testing.T, epoch uint64, tobNonce uint64) { @@ -228,7 +410,7 @@ func (be *testBackend) request(method, path string, payload any) *httptest.Respo return rr } -func (be *testBackend) RequestWithPayloadHeader(method, path string, payload any, sig string) *httptest.ResponseRecorder { +func (be *testBackend) RequestWithHeaders(method, path string, payload any, headers map[string]string) *httptest.ResponseRecorder { var req *http.Request var err error path = "/api" + path @@ -241,12 +423,20 @@ func (be *testBackend) RequestWithPayloadHeader(method, path string, payload any req, err = http.NewRequest(method, path, bytes.NewReader(payloadBytes)) } require.NoError(be.t, err) - req.Header.Set(GetPayloadHeaderRollupSig, sig) + for header, value := range headers { + req.Header.Set(header, value) + } rr := httptest.NewRecorder() be.arcadia.getRouter().ServeHTTP(rr, req) return rr } +func (be *testBackend) RequestWithPayloadHeader(method, path string, payload any, sig string) *httptest.ResponseRecorder { + return be.RequestWithHeaders(method, path, payload, map[string]string{ + GetPayloadHeaderRollupSig: sig, + }) +} + func (be *testBackend) SetupRegisteredRollups(epoch uint64, chainID *big.Int) { namespace := common.ChainIDToNamespace(chainID) rollup := &actions.RollupInfo{ @@ -1132,9 +1322,14 @@ func TestAuctionBid(t *testing.T) { func TestArcadiaBlock(t *testing.T) { path := "/arcadia/v1/validator/block" + seqNetworkID := uint32(1337) seqChainID := ids.GenerateTestID() parser := srpc.Parser{} chainIDs := []*big.Int{big.NewInt(42000), big.NewInt(43000)} + proposerSK, err := abls.NewSecretKey() + require.NoError(t, err) + proposerPK := abls.PublicFromSecretKey(proposerSK) + proposerPKBytes := proposerPK.Compress() t.Run("baseline case", func(t *testing.T) { backend := newTestBackend(t) @@ -1149,12 +1344,33 @@ func TestArcadiaBlock(t *testing.T) { maxBandwidth := uint64(20000) blockNumber := uint64(15) + + // setup seq client mock + backend.seqcli.EXPECT().GetNetworkID().Return(seqNetworkID) + backend.seqcli.EXPECT().GetChainID().Return(seqChainID) + backend.seqcli.EXPECT().ProposerAtHeight(mock.Anything, blockNumber).Return(&hrpc.Validator{ + NodeID: ids.GenerateTestNodeID(), + PublicKey: proposerPKBytes, + Weight: 1000, + }, nil) + + // prepare request and signature req := common.GetBlockPayloadFromArcadia{ MaxBandwidth: maxBandwidth, BlockNumber: blockNumber, } - rr := backend.request(http.MethodPost, path, req) + payload, err := req.Payload() + require.NoError(t, err) + uwm, err := warp.NewUnsignedMessage(seqNetworkID, seqChainID, payload) + require.NoError(t, err) + uwmBytes := uwm.Bytes() + sig := abls.Sign(proposerSK, uwmBytes) + sigStr := hexutil.Encode(sig.Compress()) + + rr := backend.RequestWithHeaders(http.MethodPost, path, req, map[string]string{ + seq.GetArcadiaBlockSignatureHeader: sigStr, + }) require.Equal(t, http.StatusOK, rr.Code) var response common.GetBlockPayloadFromArcadiaResponse @@ -1168,15 +1384,33 @@ func TestArcadiaBlock(t *testing.T) { maxBandwidth := uint64(1000) blockNumber := uint64(15) + + // setup seq client mock + backend.seqcli.EXPECT().GetNetworkID().Return(seqNetworkID) + backend.seqcli.EXPECT().GetChainID().Return(seqChainID) + backend.seqcli.EXPECT().ProposerAtHeight(mock.Anything, blockNumber).Return(&hrpc.Validator{ + NodeID: ids.GenerateTestNodeID(), + PublicKey: proposerPKBytes, + Weight: 1000, + }, nil) + req := common.GetBlockPayloadFromArcadia{ MaxBandwidth: maxBandwidth, BlockNumber: blockNumber, } + payload, err := req.Payload() + require.NoError(t, err) + uwm, err := warp.NewUnsignedMessage(seqNetworkID, seqChainID, payload) + require.NoError(t, err) + uwmBytes := uwm.Bytes() + sig := abls.Sign(proposerSK, uwmBytes) + sigStr := hexutil.Encode(sig.Compress()) - rr := backend.request(http.MethodPost, path, req) + rr := backend.RequestWithHeaders(http.MethodPost, path, req, map[string]string{ + seq.GetArcadiaBlockSignatureHeader: sigStr, + }) require.Equal(t, http.StatusNoContent, rr.Code) }) - } func TestSubmitChunkToSEQValidators(t *testing.T) { @@ -2145,6 +2379,68 @@ func TestBLSPublicKeyConversion(t *testing.T) { require.Equal(t, testBuilderPublicKey, newPk) } +func TestTxsHashStability(t *testing.T) { + epoch := uint64(100) + + originChainID := big.NewInt(45200) + originChainIDStr := hexutil.EncodeBig(originChainID) + remoteChainID := big.NewInt(45201) + remoteChainIDStr := hexutil.EncodeBig(remoteChainID) + blockNumbers := map[string]uint64{ + originChainIDStr: 100, + remoteChainIDStr: 50, + } + + testBuilderSecretKey, err := bls.GenerateRandomSecretKey() + require.NoError(t, err) + testBuilderPublicKey, err := bls.PublicKeyFromSecretKey(testBuilderSecretKey) + require.NoError(t, err) + testSeqChainID := ids.GenerateTestID() + + // constructing ToB + bundleTxs := map[string]ethtypes.Transactions{ + originChainIDStr: { + CreateEthTransfer(t, originChainID, testAccounts[0].PrivateKey, testAccounts[1].Address, 100, 1, 21000, nil), + }, + remoteChainIDStr: { + CreateEthTransfer(t, remoteChainID, testAccounts[1].PrivateKey, testAccounts[0].Address, 100, testAccounts[1].Nonce, 21000, nil), + }, + } + seqTx := CreateHypersdkBundleTx(t, testSeqChainID, bundleTxs) + oSeqTx, err := chain.MarshalTxs([]*chain.Transaction{seqTx}) + require.NoError(t, err) + + bundle := common.CrossRollupBundle{ + BundleHash: "0xbundle1", + Txs: oSeqTx, + RevertingTxHashes: nil, + } + + tobReq := CreateToBReq(t, &CreateTestBlockSubmissionOpts{ + Epoch: epoch, + ToBBlockNumber: blockNumbers, + BuilderPubkey: *testBuilderPublicKey, + BuilderSecretkey: *testBuilderSecretKey, + Bundles: []*common.CrossRollupBundle{&bundle}, + }) + + require.Greater(t, len(bundleTxs[originChainIDStr]), 0) + require.Greater(t, len(bundleTxs[remoteChainIDStr]), 0) + + bundleTxOriginHash := bundleTxs[originChainIDStr][0].Hash() + bundleTxRemoteHash := bundleTxs[remoteChainIDStr][0].Hash() + + tobReqTxs := tobReq.Chunk.ToB.GetTxs() + require.Greater(t, len(tobReqTxs[originChainIDStr]), 0) + require.Greater(t, len(tobReqTxs[remoteChainIDStr]), 0) + + tobReqTxOriginHash := tobReqTxs[originChainIDStr][0].Hash() + tobReqTxRemoteHash := tobReqTxs[remoteChainIDStr][0].Hash() + + require.Equal(t, bundleTxOriginHash, tobReqTxOriginHash) + require.Equal(t, bundleTxRemoteHash, tobReqTxRemoteHash) +} + func TestHandleSubmitNewBlockRequest(t *testing.T) { epoch := uint64(0) @@ -2425,11 +2721,16 @@ func TestHandleSubmitNewBlockRequest(t *testing.T) { originChainIDStr: 100, remoteChainIDStr: 50, } + originChainIDInt, err := common.ChainIDStrToChainID(originChainIDStr) + require.NoError(t, err) + remoteChainIDInt, err := common.ChainIDStrToChainID(remoteChainIDStr) + require.NoError(t, err) + id := ids.GenerateTestID() // constructing ToB bundleTxs := map[string]ethtypes.Transactions{ originChainIDStr: { - CreateEthTransfer(t, originChainID, testAccounts[0].PrivateKey, testAccounts[1].Address, 100, testAccounts[0].Nonce, 21000, nil), + CreateEthTransfer(t, originChainID, testAccounts[0].PrivateKey, testAccounts[1].Address, 100, 1, 21000, nil), }, remoteChainIDStr: { CreateEthTransfer(t, remoteChainID, testAccounts[1].PrivateKey, testAccounts[0].Address, 100, testAccounts[1].Nonce, 21000, nil), @@ -2454,14 +2755,62 @@ func TestHandleSubmitNewBlockRequest(t *testing.T) { }) backend := newTestBackend(t) + + // register test rollup + backend.SetupRegisteredRollups(epoch, originChainID) + backend.SetupRegisteredRollups(epoch, remoteChainID) + + // send in rob chunk for origin chain id + // needed for ToB to be accepted + robChainID := originChainIDInt + robOpts1 := &CreateTestBlockSubmissionOpts{ + Epoch: epoch, + OriginChainID: *robChainID, + RemoteChainID: *remoteChainIDInt, + SeqChainID: testSeqChainID, + BuilderPubkey: *testBuilderPublicKey, + BuilderSecretkey: *testBuilderSecretKey, + RoBBlockNumber: blockNumbers[originChainIDStr] - 1, + RoBChainID: robChainID, + ChunkID: id, + Txs: nil, + IsToB: false, + } + rob1 := backend.submitRoBChunk(t, robOpts1, chainParser) + + // send in rob chunk for remote chain id + // needed for ToB to be accepted + robChainID = remoteChainIDInt + robOpts2 := &CreateTestBlockSubmissionOpts{ + Epoch: epoch, + OriginChainID: *robChainID, + RemoteChainID: *remoteChainIDInt, + SeqChainID: testSeqChainID, + BuilderPubkey: *testBuilderPublicKey, + BuilderSecretkey: *testBuilderSecretKey, + RoBBlockNumber: blockNumbers[remoteChainIDStr] - 1, + RoBChainID: robChainID, + ChunkID: id, + Txs: nil, + IsToB: false, + } + rob2 := backend.submitRoBChunk(t, robOpts2, chainParser) + + // + // Start main TOB test case + // + backend.resetSeqClientMockExpectations(t) + + // Delay to ensure RoB is registered before checking + time.Sleep(2 * time.Second) + + chunks := backend.arcadia.chunkManager.Chunks() + fmt.Println(chunks) err = backend.arcadia.datastore.SetAuctionWinner(epoch, builderPkBytes[:]) require.NoError(t, err) redis := backend.redis redis.SetSizeTracker(backend.arcadia.sizeTracker) - // register test rollup - backend.SetupRegisteredRollups(epoch, originChainID) - backend.SetupRegisteredRollups(epoch, remoteChainID) backend.simulator.EXPECT().GetBlockNumber([]string{originChainIDStr, remoteChainIDStr}).Return(blockNumbers, nil) // set up mock expectations // shared calls for both chunks @@ -2474,6 +2823,12 @@ func TestHandleSubmitNewBlockRequest(t *testing.T) { allTxs, err := common.CollectTxsFromChunks([]*common.ArcadiaChunk{&tobReq.Chunk}) require.NoError(t, err) common.DisplayEthTxs(allTxs) + require.Greater(t, len(robOpts1.Txs), 0) + require.Greater(t, len(robOpts2.Txs), 0) + tobTxs[originChainIDStr] = slices.Insert(tobTxs[originChainIDStr], 0, robOpts1.Txs[0]) + tobTxs[remoteChainIDStr] = slices.Insert(tobTxs[remoteChainIDStr], 0, robOpts2.Txs[0]) + common.DisplayEthTxs(tobTxs) + // expectations for the first tob for domain, expectedTxs := range tobTxs { matchTxs := func(txs ethtypes.Transactions) bool { @@ -2494,13 +2849,12 @@ func TestHandleSubmitNewBlockRequest(t *testing.T) { fmt.Printf("%s, ", tx.Hash().Hex()) } fmt.Printf("]\n") - relatedAccounts, err := CollectAccountsFromTxs(expectedTxs, testAccounts) require.NoError(t, err) nonces := CollectNoncesFromEthAccounts(relatedAccounts) balances := CollectBalancesFromEthAccounts(relatedAccounts) - backend.simulator.EXPECT().GetNonces(domain, mock.MatchedBy(matchTxs), uint64ToHexString(blockNumbers[domain])).Return(nonces, nil) - backend.simulator.EXPECT().GetBalances(domain, mock.MatchedBy(matchTxs), uint64ToHexString(blockNumbers[domain])).Return(balances, nil) + backend.simulator.EXPECT().GetNonces(domain, mock.MatchedBy(matchTxs), uint64ToHexString(blockNumbers[domain]-2)).Return(nonces, nil) + backend.simulator.EXPECT().GetBalances(domain, mock.MatchedBy(matchTxs), uint64ToHexString(blockNumbers[domain]-2)).Return(balances, nil) // simulation results, only the txs from this ToB will be simulated and the txs from RoB will be filtered out callBundleRes := make([]flashbotsrpc.FlashbotsCallBundleResult, 0, len(expectedTxs)) @@ -2512,11 +2866,12 @@ func TestHandleSubmitNewBlockRequest(t *testing.T) { }) } rawExpectedTxs, err := CollectRawTxs(expectedTxs) + require.NoError(t, err) validationReq := common.BlockValidationRequest{ Txs: rawExpectedTxs, - BlockNumber: uint64ToHexString(blockNumbers[domain]), - StateBlockNumber: uint64ToHexString(blockNumbers[domain]), + BlockNumber: uint64ToHexString(blockNumbers[domain] - 2), + StateBlockNumber: uint64ToHexString(blockNumbers[domain] - 2), } backend.simulator.EXPECT(). SimBlockAndGetGasUsedForChain(mock.Anything, domain, &validationReq). @@ -2527,7 +2882,16 @@ func TestHandleSubmitNewBlockRequest(t *testing.T) { require.Equal(t, http.StatusOK, rrCode) cmTxs, cmHeights, err := backend.arcadia.chunkManager.Txs() require.NoError(t, err) - TxsTheSame(t, bundleTxs, cmTxs) + require.NotNil(t, rob1) + require.NotNil(t, rob2) + allChunksTxs, err := common.CollectTxsFromChunks([]*common.ArcadiaChunk{&tobReq.Chunk, &rob1.Chunk, &rob2.Chunk}) + require.NoError(t, err) + + common.DisplayEthTxs(allChunksTxs) + common.DisplayEthTxs(cmTxs) + TxsTheSameUnordered(t, allChunksTxs, cmTxs) + blockNumbers[originChainIDStr] = blockNumbers[originChainIDStr] - 2 + blockNumbers[remoteChainIDStr] = blockNumbers[remoteChainIDStr] - 2 require.Equal(t, blockNumbers, cmHeights) for _, bundle := range tobReq.Chunk.ToB.GetBundles() { @@ -2605,7 +2969,7 @@ func TestHandleSubmitNewBlockRequest(t *testing.T) { // constructing ToB bundleTxs := map[string]ethtypes.Transactions{ originChainIDStr: { - CreateEthTransfer(t, originChainID, testAccounts[0].PrivateKey, testAccounts[1].Address, 100, testAccounts[0].Nonce, 21000, nil), + CreateEthTransfer(t, originChainID, testAccounts[0].PrivateKey, testAccounts[1].Address, 100, testAccounts[0].Nonce+1, 21000, nil), }, remoteChainIDStr: { CreateEthTransfer(t, remoteChainID, testAccounts[1].PrivateKey, testAccounts[0].Address, 100, testAccounts[1].Nonce, 21000, nil), @@ -2632,7 +2996,7 @@ func TestHandleSubmitNewBlockRequest(t *testing.T) { // constructing RoB robTxs := ethtypes.Transactions{ // conflicting tx with prev ToB - CreateEthTransfer(t, originChainID, testAccounts[0].PrivateKey, testAccounts[1].Address, 20, testAccounts[0].Nonce, 21000, nil), + CreateEthTransfer(t, originChainID, testAccounts[0].PrivateKey, testAccounts[1].Address, 20, testAccounts[0].Nonce+1, 21000, nil), } robReq := CreateRoBReq(t, &CreateTestBlockSubmissionOpts{ Epoch: epoch, @@ -2645,9 +3009,14 @@ func TestHandleSubmitNewBlockRequest(t *testing.T) { }) backend := newTestBackend(t) + + // sends two robs to warmup the tob + rob1, rob2 := backend.setupRoBsForToBTest(t) + err = backend.arcadia.datastore.SetAuctionWinner(epoch, builderPkBytes[:]) require.NoError(t, err) redis := backend.redis + redis.SetSizeTracker(backend.arcadia.sizeTracker) backend.simulator.EXPECT().GetBlockNumber([]string{originChainIDStr, remoteChainIDStr}).Return(blockNumbers, nil) @@ -2660,10 +3029,10 @@ func TestHandleSubmitNewBlockRequest(t *testing.T) { backend.seqcli.EXPECT().Parser().Return(chainParser) t.Log("========setup & send TOB============") - tobTxs, err := common.CollectTxsFromChunks([]*common.ArcadiaChunk{&tobReq.Chunk}) + tobTxs, err := common.CollectTxsFromChunks([]*common.ArcadiaChunk{&rob1.Chunk, &rob2.Chunk, &tobReq.Chunk}) require.NoError(t, err) common.DisplayEthTxs(tobTxs) - allTxs, err := common.CollectTxsFromChunks([]*common.ArcadiaChunk{&tobReq.Chunk, &robReq.Chunk}) + allTxs, err := common.CollectTxsFromChunks([]*common.ArcadiaChunk{&rob1.Chunk, &rob2.Chunk, &tobReq.Chunk}) require.NoError(t, err) common.DisplayEthTxs(allTxs) @@ -2720,23 +3089,15 @@ func TestHandleSubmitNewBlockRequest(t *testing.T) { require.Equal(t, http.StatusOK, rrCode) cmTxs, _, err := backend.arcadia.chunkManager.Txs() require.NoError(t, err) - TxsTheSame(t, bundleTxs, cmTxs) + TxsTheSameUnordered(t, allTxs, cmTxs) - // for the rob - t.Log("========setup & send RoB============") - for domain, expectedTxs := range allTxs { - matchTxs := func(txs ethtypes.Transactions) bool { - if len(txs) != len(expectedTxs) { - return false - } - for i, tx := range txs { - if tx.Hash().Hex() != expectedTxs[i].Hash().Hex() { - return false - } - } - return true - } + // for the rob after tob + allTxs2, err := common.CollectTxsFromChunks([]*common.ArcadiaChunk{&rob1.Chunk, &rob2.Chunk, &tobReq.Chunk, &robReq.Chunk}) + require.NoError(t, err) + common.DisplayEthTxs(allTxs) + t.Log("========setup & send RoB after ToB============") + for domain, expectedTxs := range allTxs2 { fmt.Printf("expected txs for domain: %s\n", domain) fmt.Printf("[") for _, tx := range expectedTxs { @@ -2746,8 +3107,11 @@ func TestHandleSubmitNewBlockRequest(t *testing.T) { relatedAccounts, err := CollectAccountsFromTxs(expectedTxs, testAccounts) require.NoError(t, err) + nonces := CollectNoncesFromEthAccounts(relatedAccounts) balances := CollectBalancesFromEthAccounts(relatedAccounts) + + matchTxs := ExpectedMatchTxs(expectedTxs) backend.simulator.EXPECT().GetNonces(domain, mock.MatchedBy(matchTxs), uint64ToHexString(blockNumbers[domain])).Return(nonces, nil) backend.simulator.EXPECT().GetBalances(domain, mock.MatchedBy(matchTxs), uint64ToHexString(blockNumbers[domain])).Return(balances, nil) } @@ -2763,7 +3127,7 @@ func TestHandleSubmitNewBlockRequest(t *testing.T) { t.Log("validTxs from chunk manager") common.DisplayEthTxs(validTxs) - TxsTheSame(t, bundleTxs, validTxs) + TxsTheSameUnordered(t, allTxs, validTxs) require.Equal(t, blockNumbers, cmHeights) for _, bundle := range tobReq.Chunk.ToB.GetBundles() { @@ -2773,7 +3137,7 @@ func TestHandleSubmitNewBlockRequest(t *testing.T) { } }) - t.Run("incoming ToB conflicts with previous RoBs, txs with incosecutive nonces", func(t *testing.T) { + t.Run("incoming ToB conflicts with previous RoBs, txs with inconsecutive nonces", func(t *testing.T) { originChainID := big.NewInt(45200) originChainIDStr := hexutil.EncodeBig(originChainID) remoteChainID := big.NewInt(45201) @@ -2783,28 +3147,14 @@ func TestHandleSubmitNewBlockRequest(t *testing.T) { remoteChainIDStr: 50, } - // constructing RoB - robTxs := ethtypes.Transactions{ - // conflicting tx with prev ToB - CreateEthTransfer(t, originChainID, testAccounts[0].PrivateKey, testAccounts[1].Address, 20, testAccounts[0].Nonce, 21000, nil), - } - robReq := CreateRoBReq(t, &CreateTestBlockSubmissionOpts{ - Epoch: epoch, - SeqChainID: testSeqChainID, - RoBChainID: originChainID, - RoBBlockNumber: blockNumbers[originChainIDStr] - 1, - BuilderPubkey: *testBuilderPublicKey, - BuilderSecretkey: *testBuilderSecretKey, - Txs: robTxs, - }) - // constructing ToB + inconsecutiveNonceRemoteTx := testAccounts[1].Nonce + 5 bundleTxs := map[string]ethtypes.Transactions{ originChainIDStr: { - CreateEthTransfer(t, originChainID, testAccounts[0].PrivateKey, testAccounts[1].Address, 100, testAccounts[0].Nonce, 21000, nil), + CreateEthTransfer(t, originChainID, testAccounts[0].PrivateKey, testAccounts[1].Address, 100, testAccounts[0].Nonce+1, 21000, nil), }, remoteChainIDStr: { - CreateEthTransfer(t, remoteChainID, testAccounts[1].PrivateKey, testAccounts[0].Address, 100, testAccounts[1].Nonce, 21000, nil), + CreateEthTransfer(t, remoteChainID, testAccounts[1].PrivateKey, testAccounts[0].Address, 100, inconsecutiveNonceRemoteTx, 21000, nil), }, } seqTx := CreateHypersdkBundleTx(t, testSeqChainID, bundleTxs) @@ -2819,14 +3169,19 @@ func TestHandleSubmitNewBlockRequest(t *testing.T) { tobReq := CreateToBReq(t, &CreateTestBlockSubmissionOpts{ Epoch: epoch, + OriginChainID: *originChainID, + RemoteChainID: *remoteChainID, ToBBlockNumber: blockNumbers, BuilderPubkey: *testBuilderPublicKey, BuilderSecretkey: *testBuilderSecretKey, Bundles: []*common.CrossRollupBundle{&bundle}, + IsToB: true, }) // instantiate backend backend := newTestBackend(t) + // sends two robs to warmup the tob + rob1, rob2 := backend.setupRoBsForToBTest(t) err = backend.arcadia.datastore.SetAuctionWinner(epoch, builderPkBytes[:]) require.NoError(t, err) redis := backend.redis @@ -2837,13 +3192,22 @@ func TestHandleSubmitNewBlockRequest(t *testing.T) { backend.SetupRegisteredRollups(epoch, originChainID) backend.SetupRegisteredRollups(epoch, remoteChainID) - // set up mock expectations - // shared calls for both chunks - backend.seqcli.EXPECT().Parser().Return(chainParser) - fmt.Println("========setup & send RoB============") - for domain, expectedTxs := range map[string]ethtypes.Transactions{ - originChainIDStr: robTxs, - } { + t.Log("========setup & send TOB============") + tobTxs, err := common.CollectTxsFromChunks([]*common.ArcadiaChunk{&tobReq.Chunk}) + require.NoError(t, err) + + tobTxs[originChainIDStr] = slices.Insert(tobTxs[originChainIDStr], 0, rob1.Chunk.RoB.GetTxs()[0]) + tobTxs[remoteChainIDStr] = slices.Insert(tobTxs[remoteChainIDStr], 0, rob2.Chunk.RoB.GetTxs()[0]) + + allTxs, err := common.CollectTxsFromChunks([]*common.ArcadiaChunk{&rob1.Chunk, &rob2.Chunk, &tobReq.Chunk}) + require.NoError(t, err) + t.Log("all txs") + common.DisplayEthTxs(allTxs) + + robTxs, err := common.CollectTxsFromChunks([]*common.ArcadiaChunk{&rob1.Chunk, &rob2.Chunk}) + require.NoError(t, err) + + for domain, expectedTxs := range tobTxs { matchTxs := func(txs ethtypes.Transactions) bool { if len(txs) != len(expectedTxs) { return false @@ -2862,13 +3226,6 @@ func TestHandleSubmitNewBlockRequest(t *testing.T) { fmt.Printf("%s, ", tx.Hash().Hex()) } fmt.Printf("]\n") - - relatedAccounts, err := CollectAccountsFromTxs(expectedTxs, testAccounts) - require.NoError(t, err) - nonces := CollectNoncesFromEthAccounts(relatedAccounts) - balances := CollectBalancesFromEthAccounts(relatedAccounts) - backend.simulator.EXPECT().GetNonces(domain, mock.MatchedBy(matchTxs), uint64ToHexString(blockNumbers[domain]-2)).Return(nonces, nil) - backend.simulator.EXPECT().GetBalances(domain, mock.MatchedBy(matchTxs), uint64ToHexString(blockNumbers[domain]-2)).Return(balances, nil) // simulation results, only the txs from this RoB will be simulated and the txs from ToB will be filtered out callBundleRes := make([]flashbotsrpc.FlashbotsCallBundleResult, 0, len(expectedTxs)) for _, tx := range expectedTxs { @@ -2878,72 +3235,42 @@ func TestHandleSubmitNewBlockRequest(t *testing.T) { Revert: "", }) } - rawExpectedTxs, err := CollectRawTxs(expectedTxs) + + robDomainTxs := robTxs[domain] + rawExpectedTxs, err := CollectRawTxs(robDomainTxs) require.NoError(t, err) validationReq := common.BlockValidationRequest{ Txs: rawExpectedTxs, - BlockNumber: uint64ToHexString(blockNumbers[domain] - 2), - StateBlockNumber: uint64ToHexString(blockNumbers[domain] - 2), + BlockNumber: uint64ToHexString(blockNumbers[domain]), + StateBlockNumber: uint64ToHexString(blockNumbers[domain]), } backend.simulator.EXPECT(). SimBlockAndGetGasUsedForChain(mock.Anything, domain, &validationReq). Return(100, callBundleRes, nil) - } - - rrCode1 := processBlockRequest(backend, robReq) - require.Equal(t, http.StatusOK, rrCode1) - - t.Log("========setup & send TOB============") - allTxs, err := common.CollectTxsFromChunks([]*common.ArcadiaChunk{&robReq.Chunk, &tobReq.Chunk}) - require.NoError(t, err) - t.Log("all txs") - common.DisplayEthTxs(allTxs) - for domain, expectedTxs := range allTxs { - matchTxs := func(txs ethtypes.Transactions) bool { - if len(txs) != len(expectedTxs) { - return false - } - for i, tx := range txs { - if tx.Hash().Hex() != expectedTxs[i].Hash().Hex() { - return false - } - } - return true - } - - fmt.Printf("expected txs for domain: %s\n", domain) - fmt.Printf("[") - for _, tx := range expectedTxs { - fmt.Printf("%s, ", tx.Hash().Hex()) - } - fmt.Printf("]\n") relatedAccounts, err := CollectAccountsFromTxs(expectedTxs, testAccounts) require.NoError(t, err) nonces := CollectNoncesFromEthAccounts(relatedAccounts) balances := CollectBalancesFromEthAccounts(relatedAccounts) - if domain == originChainIDStr { - backend.simulator.EXPECT().GetNonces(domain, mock.MatchedBy(matchTxs), uint64ToHexString(blockNumbers[domain]-2)).Return(nonces, nil) - backend.simulator.EXPECT().GetBalances(domain, mock.MatchedBy(matchTxs), uint64ToHexString(blockNumbers[domain]-2)).Return(balances, nil) - } else { - backend.simulator.EXPECT().GetNonces(domain, mock.MatchedBy(matchTxs), uint64ToHexString(blockNumbers[domain])).Return(nonces, nil) - backend.simulator.EXPECT().GetBalances(domain, mock.MatchedBy(matchTxs), uint64ToHexString(blockNumbers[domain])).Return(balances, nil) - } + backend.simulator.EXPECT().GetNonces(domain, mock.MatchedBy(matchTxs), uint64ToHexString(blockNumbers[domain])).Return(nonces, nil) + backend.simulator.EXPECT().GetBalances(domain, mock.MatchedBy(matchTxs), uint64ToHexString(blockNumbers[domain])).Return(balances, nil) } + rrCode := processBlockRequest(backend, tobReq) - require.Equal(t, http.StatusOK, rrCode) + require.Equal(t, http.StatusNoContent, rrCode) - require.Equal(t, 2, len(backend.arcadia.chunkManager.Chunks())) + // TODO: Verify the below + require.Equal(t, 1, len(backend.arcadia.chunkManager.Chunks())) validTxs, cmHeights, err := backend.arcadia.chunkManager.Txs() require.NoError(t, err) - TxsTheSame(t, map[string]ethtypes.Transactions{ - originChainIDStr: robTxs, - }, validTxs) + TxsTheSameUnordered(t, robTxs, validTxs) expectedLowestHeights := maps.Clone(blockNumbers) + // TODO: below check is not needed since we submit a rob per chainID so 0x91 will have rob tx. // delete the remote block number since ToB's conflicting with previous RoBs and bundle got removed - delete(expectedLowestHeights, remoteChainIDStr) - expectedLowestHeights[originChainIDStr] = blockNumbers[originChainIDStr] - 2 + //delete(expectedLowestHeights, remoteChainIDStr) + //expectedLowestHeights[originChainIDStr] = blockNumbers[originChainIDStr] + require.Equal(t, expectedLowestHeights, cmHeights) for _, bundle := range tobReq.Chunk.ToB.GetBundles() { @@ -2966,7 +3293,7 @@ func TestHandleSubmitNewBlockRequest(t *testing.T) { // constructing ToB bundleTxs := map[string]ethtypes.Transactions{ originChainIDStr: { - CreateEthTransfer(t, originChainID, testAccounts[0].PrivateKey, testAccounts[1].Address, 100, testAccounts[0].Nonce, 21000, nil), + CreateEthTransfer(t, originChainID, testAccounts[0].PrivateKey, testAccounts[1].Address, 100, testAccounts[0].Nonce+1, 21000, nil), }, remoteChainIDStr: { CreateEthTransfer(t, remoteChainID, testAccounts[1].PrivateKey, testAccounts[0].Address, 100, testAccounts[1].Nonce, 21000, nil), @@ -2993,7 +3320,7 @@ func TestHandleSubmitNewBlockRequest(t *testing.T) { // constructing RoB robTxs := ethtypes.Transactions{ // conflicting tx with prev ToB - CreateEthTransfer(t, originChainID, testAccounts[0].PrivateKey, testAccounts[1].Address, 20, testAccounts[0].Nonce+1, 21000, nil), + CreateEthTransfer(t, originChainID, testAccounts[0].PrivateKey, testAccounts[1].Address, 20, testAccounts[0].Nonce+2, 21000, nil), } robReq := CreateRoBReq(t, &CreateTestBlockSubmissionOpts{ Epoch: epoch, @@ -3006,6 +3333,8 @@ func TestHandleSubmitNewBlockRequest(t *testing.T) { }) backend := newTestBackend(t) + // sends two robs to warmup the tob + rob1, rob2 := backend.setupRoBsForToBTest(t) err = backend.arcadia.datastore.SetAuctionWinner(epoch, builderPkBytes[:]) require.NoError(t, err) redis := backend.redis @@ -3021,10 +3350,10 @@ func TestHandleSubmitNewBlockRequest(t *testing.T) { backend.seqcli.EXPECT().Parser().Return(chainParser) t.Log("========setup & send TOB============") - tobTxs, err := common.CollectTxsFromChunks([]*common.ArcadiaChunk{&tobReq.Chunk}) + tobTxs, err := common.CollectTxsFromChunks([]*common.ArcadiaChunk{&rob1.Chunk, &rob2.Chunk, &tobReq.Chunk}) require.NoError(t, err) common.DisplayEthTxs(tobTxs) - allTxs, err := common.CollectTxsFromChunks([]*common.ArcadiaChunk{&tobReq.Chunk, &robReq.Chunk}) + allTxs, err := common.CollectTxsFromChunks([]*common.ArcadiaChunk{&rob1.Chunk, &rob2.Chunk, &tobReq.Chunk, &robReq.Chunk}) require.NoError(t, err) common.DisplayEthTxs(allTxs) // expectations for the first tob @@ -3075,12 +3404,8 @@ func TestHandleSubmitNewBlockRequest(t *testing.T) { SimBlockAndGetGasUsedForChain(mock.Anything, domain, &validationReq). Return(100, callBundleRes, nil) } - rrCode := processBlockRequest(backend, tobReq) require.Equal(t, http.StatusOK, rrCode) - cmTxs, _, err := backend.arcadia.chunkManager.Txs() - require.NoError(t, err) - TxsTheSame(t, bundleTxs, cmTxs) t.Log("========setup & send RoB============") for domain, expectedTxs := range allTxs { @@ -3162,7 +3487,6 @@ func TestHandleSubmitNewBlockRequest(t *testing.T) { t.Log("validTxs from chunk manager") common.DisplayEthTxs(validTxs) - TxsTheSame(t, bundleTxs, validTxs) require.Equal(t, blockNumbers, cmHeights) for _, bundle := range tobReq.Chunk.ToB.GetBundles() { status, err := backend.redis.GetBundleStatus(bundle.BundleHash) @@ -3210,6 +3534,8 @@ func TestHandleSubmitNewBlockRequest(t *testing.T) { }) backend := newTestBackend(t) + // sends two robs to warmup the tob + rob1, rob2 := backend.setupRoBsForToBTest(t) err = backend.arcadia.datastore.SetAuctionWinner(epoch, builderPkBytes[:]) require.NoError(t, err) redis := backend.redis @@ -3230,6 +3556,8 @@ func TestHandleSubmitNewBlockRequest(t *testing.T) { allTxs, err := common.CollectTxsFromChunks([]*common.ArcadiaChunk{&tobReq.Chunk}) require.NoError(t, err) common.DisplayEthTxs(allTxs) + tobTxs[originChainIDStr] = slices.Insert(tobTxs[originChainIDStr], 0, rob1.Chunk.RoB.GetTxs()[0]) + tobTxs[remoteChainIDStr] = slices.Insert(tobTxs[remoteChainIDStr], 0, rob2.Chunk.RoB.GetTxs()[0]) // expectations for the first tob for domain, expectedTxs := range tobTxs { matchTxs := func(txs ethtypes.Transactions) bool { @@ -3258,13 +3586,12 @@ func TestHandleSubmitNewBlockRequest(t *testing.T) { backend.simulator.EXPECT().GetNonces(domain, mock.MatchedBy(matchTxs), uint64ToHexString(blockNumbers[domain])).Return(nonces, nil) backend.simulator.EXPECT().GetBalances(domain, mock.MatchedBy(matchTxs), uint64ToHexString(blockNumbers[domain])).Return(balances, nil) } + backend.setupSimBlockAndGetUsedExpectation(t, rob1.Chunk.RoB.GetTxs(), blockNumbers[originChainIDStr], originChainIDStr) + backend.setupSimBlockAndGetUsedExpectation(t, rob2.Chunk.RoB.GetTxs(), blockNumbers[remoteChainIDStr], remoteChainIDStr) rrCode := processBlockRequest(backend, tobReq) - require.Equal(t, http.StatusOK, rrCode) - cmTxs, _, err := backend.arcadia.chunkManager.Txs() - require.NoError(t, err) - TxsTheSame(t, nil, cmTxs) - require.Equal(t, 2, len(backend.arcadia.chunkManager.Chunks())) + require.Equal(t, http.StatusNoContent, rrCode) + require.Equal(t, 1, len(backend.arcadia.chunkManager.Chunks())) for _, bundle := range tobReq.Chunk.ToB.GetBundles() { status, err := backend.redis.GetBundleStatus(bundle.BundleHash) require.NoError(t, err) @@ -3282,7 +3609,7 @@ func TestHandleSubmitNewBlockRequest(t *testing.T) { remoteChainIDStr: 50, } - originTx := CreateEthTransfer(t, originChainID, testAccounts[0].PrivateKey, testAccounts[1].Address, 100, testAccounts[0].Nonce, 21000, nil) + originTx := CreateEthTransfer(t, originChainID, testAccounts[0].PrivateKey, testAccounts[1].Address, 100, 1, 21000, nil) // constructing ToB bundleTxs := map[string]ethtypes.Transactions{ originChainIDStr: { @@ -3312,22 +3639,11 @@ func TestHandleSubmitNewBlockRequest(t *testing.T) { Bundles: []*common.CrossRollupBundle{&bundle}, }) - // constructing RoB - robTxs := ethtypes.Transactions{ - // conflicting tx with prev ToB - CreateEthTransfer(t, originChainID, testAccounts[0].PrivateKey, testAccounts[1].Address, 20, testAccounts[0].Nonce, 21000, nil), - } - robReq := CreateRoBReq(t, &CreateTestBlockSubmissionOpts{ - Epoch: epoch, - SeqChainID: testSeqChainID, - RoBChainID: originChainID, - RoBBlockNumber: blockNumbers[originChainIDStr] + 1, - BuilderPubkey: *testBuilderPublicKey, - BuilderSecretkey: *testBuilderSecretKey, - Txs: robTxs, - }) - backend := newTestBackend(t) + + // sends two robs to warmup the tob + rob1, rob2 := backend.setupRoBsForToBTest(t) + err = backend.arcadia.datastore.SetAuctionWinner(epoch, builderPkBytes[:]) require.NoError(t, err) redis := backend.redis @@ -3345,24 +3661,14 @@ func TestHandleSubmitNewBlockRequest(t *testing.T) { tobTxs, err := common.CollectTxsFromChunks([]*common.ArcadiaChunk{&tobReq.Chunk}) require.NoError(t, err) common.DisplayEthTxs(tobTxs) - allTxs, err := common.CollectTxsFromChunks([]*common.ArcadiaChunk{&tobReq.Chunk, &robReq.Chunk}) + allTxs, err := common.CollectTxsFromChunks([]*common.ArcadiaChunk{&tobReq.Chunk, &rob1.Chunk, &rob2.Chunk}) require.NoError(t, err) common.DisplayEthTxs(allTxs) + tobTxs[originChainIDStr] = slices.Insert(tobTxs[originChainIDStr], 0, rob1.Chunk.RoB.GetTxs()[0]) + tobTxs[remoteChainIDStr] = slices.Insert(tobTxs[remoteChainIDStr], 0, rob2.Chunk.RoB.GetTxs()[0]) // expectations for the first tob for domain, expectedTxs := range tobTxs { - matchTxs := func(txs ethtypes.Transactions) bool { - - if len(txs) != len(expectedTxs) { - return false - } - for i, tx := range txs { - if tx.Hash().Hex() != expectedTxs[i].Hash().Hex() { - return false - } - } - return true - } fmt.Printf("expected txs for domain: %s\n", domain) fmt.Printf("[") for _, tx := range expectedTxs { @@ -3375,6 +3681,8 @@ func TestHandleSubmitNewBlockRequest(t *testing.T) { nonces := CollectNoncesFromEthAccounts(relatedAccounts) balances := CollectBalancesFromEthAccounts(relatedAccounts) fmt.Printf("expected balance for domain(%s): %+v\n", domain, balances) + + matchTxs := ExpectedMatchTxs(expectedTxs) backend.simulator.EXPECT().GetNonces(domain, mock.MatchedBy(matchTxs), uint64ToHexString(blockNumbers[domain])).Return(nonces, nil) backend.simulator.EXPECT().GetBalances(domain, mock.MatchedBy(matchTxs), uint64ToHexString(blockNumbers[domain])).Return(balances, nil) @@ -3412,7 +3720,9 @@ func TestHandleSubmitNewBlockRequest(t *testing.T) { cmTxs, _, err := backend.arcadia.chunkManager.Txs() require.NoError(t, err) - TxsTheSame(t, bundleTxs, cmTxs) + + TxsTheSameUnordered(t, allTxs, cmTxs) + for _, bundle := range tobReq.Chunk.ToB.GetBundles() { status, err := backend.redis.GetBundleStatus(bundle.BundleHash) require.NoError(t, err) @@ -3736,6 +4046,7 @@ func TestWarmupPeriodAdvancement(t *testing.T) { // This tests assumes that the chunks have been preconf'd and the payload(wherever it has to go) // is in the right place in the database that we can retrieve. +// TODO: Verify this one func TestGetPayload(t *testing.T) { // Setup backend with headSlot and genesisTime epoch := uint64(0) @@ -3769,6 +4080,7 @@ func TestGetPayload(t *testing.T) { processBlockRequest := func(backend *testBackend, payload *common.GetPayloadRequest, sigStr string) (int, []byte) { // new HTTP req rr := backend.RequestWithPayloadHeader(http.MethodPost, pathGetPayload, payload, sigStr) + require.Equal(t, http.StatusOK, rr.Code) if rr.Body != nil { return rr.Code, rr.Body.Bytes() @@ -3882,8 +4194,8 @@ func TestGetPayload(t *testing.T) { sigBytes := sig.Bytes() sigStr := hexutil.Encode(sigBytes[:]) - rrCode, _ := processBlockRequest(backend, payload, sigStr) - require.Equal(t, http.StatusServiceUnavailable, rrCode) + rr := backend.RequestWithPayloadHeader(http.MethodPost, pathGetPayload, payload, sigStr) + require.Equal(t, http.StatusServiceUnavailable, rr.Code) }) t.Run("case 1: 1 rollup, only rob", func(t *testing.T) { @@ -5080,6 +5392,8 @@ func TestOverallFlow(t *testing.T) { } for _, tc := range tests { + t.Log("*** Running Overall Flow param disable_redis [" + strconv.FormatBool(tc.disableRedis) + "] ***") + backend := newTestBackendWithFlags(t, tc.disableRedis) arcadia := backend.GetArcadia() auctionBidPath := "/arcadia/v1/builder/auction_bid" @@ -5116,21 +5430,49 @@ func TestOverallFlow(t *testing.T) { remoteChainIDStr: 50, } - // test ethereum signing keys - robTxs := ethtypes.Transactions{ - CreateEthTransfer(t, originChainID, testAccounts[0].PrivateKey, testAccounts[1].Address, 20, testAccounts[0].Nonce, 21000, nil), - } + // send in rob chunk for origin chain id + // needed for ToB to be accepted + testSeqChainID := ids.GenerateTestID() + testBuilderSecretKey, err := bls.GenerateRandomSecretKey() + require.NoError(t, err) + testBuilderPublicKey, err := bls.PublicKeyFromSecretKey(testBuilderSecretKey) + require.NoError(t, err) + id := ids.GenerateTestID() - // constructing RoB - robReq := CreateRoBReq(t, &CreateTestBlockSubmissionOpts{ - Epoch: epoch + 2, - SeqChainID: seqChainID, + robOpts1 := &CreateTestBlockSubmissionOpts{ + Epoch: epoch, + OriginChainID: *originChainID, + RemoteChainID: *remoteChainID, + SeqChainID: testSeqChainID, + BuilderPubkey: *testBuilderPublicKey, + BuilderSecretkey: *testBuilderSecretKey, + RoBBlockNumber: blockNumbers[originChainIDStr] + 1, RoBChainID: originChainID, - RoBBlockNumber: blockNumbers[originChainIDStr] - 1, - BuilderPubkey: *builderPK2, - BuilderSecretkey: *builderSK2, - Txs: robTxs, - }) + ChunkID: id, + Txs: ethtypes.Transactions{ + CreateEthTransfer(t, originChainID, testAccounts[0].PrivateKey, testAccounts[1].Address, 20, testAccounts[0].Nonce, 21000, nil), + }, + IsToB: false, + } + robReq := backend.submitRoBChunk(t, robOpts1, chainParser) + fmt.Println(robReq) + + robOpts2 := &CreateTestBlockSubmissionOpts{ + Epoch: epoch, + OriginChainID: *remoteChainID, + RemoteChainID: *originChainID, + SeqChainID: testSeqChainID, + BuilderPubkey: *testBuilderPublicKey, + BuilderSecretkey: *testBuilderSecretKey, + RoBBlockNumber: blockNumbers[originChainIDStr] + 1, + RoBChainID: remoteChainID, + ChunkID: id, + Txs: ethtypes.Transactions{ + CreateEthTransfer(t, remoteChainID, testAccounts[0].PrivateKey, testAccounts[1].Address, 20, testAccounts[0].Nonce, 21000, nil), + }, + IsToB: false, + } + robReq2 := backend.submitRoBChunk(t, robOpts2, chainParser) // constructing tob1 bundle1Txs := map[string]ethtypes.Transactions{ @@ -5283,12 +5625,6 @@ func TestOverallFlow(t *testing.T) { backend.seqcli.EXPECT().GetNetworkID().Return(networkID).Maybe() backend.seqcli.EXPECT().CurrentValidators(mock.Anything).Return(validators) - // Shut down mock Redis after rollups register since Arcadia relies on SEQ state for valid rollup list, not postgres in this case - //err = backend.redis.Close() - //require.NoError(t, err) - - //backend.useRedis = false - var conns = make([]*websocket.Conn, 0) t.Run("subscribe multiple seq validators to arcadia in order to receive chunk(s)", func(t *testing.T) { path := "/ws/arcadia/v1/validator/subscribe" @@ -5550,86 +5886,30 @@ func TestOverallFlow(t *testing.T) { // shared calls for both chunks backend.seqcli.EXPECT().Parser().Return(chainParser) - t.Log("=========setup RoB simulation===========") - for domain, expectedTxs := range map[string]ethtypes.Transactions{ - originChainIDStr: robTxs, - } { - matchTxs := func(txs ethtypes.Transactions) bool { - if len(txs) != len(expectedTxs) { - return false - } - for i, tx := range txs { - if tx.Hash().Hex() != expectedTxs[i].Hash().Hex() { - return false - } - } - return true - } - - relatedAccounts, err := CollectAccountsFromTxs(expectedTxs, testAccounts) - require.NoError(t, err) - nonces := CollectNoncesFromEthAccounts(relatedAccounts) - balances := CollectBalancesFromEthAccounts(relatedAccounts) - - backend.simulator.EXPECT().GetNonces(domain, mock.MatchedBy(matchTxs), uint64ToHexString(blockNumbers[domain]-2)).Once().Return(nonces, nil) - backend.simulator.EXPECT().GetBalances(domain, mock.MatchedBy(matchTxs), uint64ToHexString(blockNumbers[domain]-2)).Once().Return(balances, nil) - - // simulation results, only the txs from this RoB will be simulated and the txs from ToB will be filtered out - callBundleRes := make([]flashbotsrpc.FlashbotsCallBundleResult, 0, len(expectedTxs)) - for _, tx := range expectedTxs { - callBundleRes = append(callBundleRes, flashbotsrpc.FlashbotsCallBundleResult{ - TxHash: tx.Hash().Hex(), - Error: "", - Revert: "", - }) - } - rawExpectedTxs, err := CollectRawTxs(expectedTxs) - require.NoError(t, err) - validationReq := common.BlockValidationRequest{ - Txs: rawExpectedTxs, - BlockNumber: uint64ToHexString(blockNumbers[domain] - 2), - StateBlockNumber: uint64ToHexString(blockNumbers[domain] - 2), - } - backend.simulator.EXPECT(). - SimBlockAndGetGasUsedForChain(mock.Anything, domain, &validationReq). - Once(). - Return(uint64(100), callBundleRes, nil) - } - t.Log("=========setup simulation mocks before tob1==============") tobChunks := []*common.ArcadiaChunk{&tobReq.Chunk, &tobReq2.Chunk, &tobReq3.Chunk} for i, tobChunk := range tobChunks { backend.simulator.EXPECT().GetBlockNumber(tobChunk.ToB.Domains()).Return(blockNumbers, nil) - chunksSoFar := []*common.ArcadiaChunk{&robReq.Chunk} + chunksSoFar := []*common.ArcadiaChunk{&robReq.Chunk, &robReq2.Chunk} chunksSoFar = append(chunksSoFar, tobChunks[:i+1]...) t.Logf("num chunks to set mocks: %d", len(chunksSoFar)) allTxsSoFar, err := common.CollectTxsFromChunks(chunksSoFar) require.NoError(t, err) + // setup expectations for simulation for domain, expectedTxs := range allTxsSoFar { - matchTxs := func(txs ethtypes.Transactions) bool { - if len(txs) != len(expectedTxs) { - return false - } - for i, tx := range txs { - if tx.Hash().Hex() != expectedTxs[i].Hash().Hex() { - return false - } - } - return true - } - blockNumberForDomain := blockNumbers[domain] if domain == robReq.Chunk.RoB.ChainID { - blockNumberForDomain = blockNumbers[domain] - 2 + blockNumberForDomain = blockNumbers[domain] } relatedAccounts, err := CollectAccountsFromTxs(expectedTxs, testAccounts) require.NoError(t, err) nonces := CollectNoncesFromEthAccounts(relatedAccounts) balances := CollectBalancesFromEthAccounts(relatedAccounts) - backend.simulator.EXPECT().GetNonces(domain, mock.MatchedBy(matchTxs), uint64ToHexString(blockNumberForDomain)).Once().Return(nonces, nil) - backend.simulator.EXPECT().GetBalances(domain, mock.MatchedBy(matchTxs), uint64ToHexString(blockNumberForDomain)).Once().Return(balances, nil) + blockNumber := uint64ToHexString(blockNumbers[domain]) + backend.simulator.EXPECT().GetNonces(domain, mock.MatchedBy(ExpectedMatchTxs(expectedTxs)), blockNumber).Return(nonces, nil).Maybe() + backend.simulator.EXPECT().GetBalances(domain, mock.MatchedBy(ExpectedMatchTxs(expectedTxs)), uint64ToHexString(blockNumbers[domain])).Return(balances, nil).Maybe() // simulation results, only the txs from this ToB will be simulated and the txs from RoB will be filtered out callBundleRes := make([]flashbotsrpc.FlashbotsCallBundleResult, 0, len(expectedTxs)) @@ -5662,9 +5942,6 @@ func TestOverallFlow(t *testing.T) { require.NoError(t, err) t.Log(winner) - rrCode1 := processBlockRequest(backend, robReq) - require.Equal(t, http.StatusOK, rrCode1) - rrCode2 := processBlockRequest(backend, tobReq) require.Equal(t, http.StatusOK, rrCode2) @@ -5673,6 +5950,9 @@ func TestOverallFlow(t *testing.T) { rrCode4 := processBlockRequest(backend, tobReq3) require.Equal(t, http.StatusOK, rrCode4) + + backend.simulator.AssertExpectations(t) + backend.seqcli.AssertExpectations(t) }) t.Run("receive preconfs for pending blocks and construct payloads"+tc.name(), func(t *testing.T) { @@ -5777,9 +6057,7 @@ func TestOverallFlow(t *testing.T) { processBlockRequest := func(backend *testBackend, payload *common.GetPayloadRequest, sigStr string) (int, []byte) { // new HTTP req rr := backend.RequestWithPayloadHeader(http.MethodPost, pathGetPayload, payload, sigStr) - if rr.Body != nil { - t.Log("error: ", rr.Body.String()) return rr.Code, rr.Body.Bytes() } else { return rr.Code, nil @@ -5826,42 +6104,57 @@ func TestOverallFlow(t *testing.T) { err = backend.arcadia.redis.SetPayloadTxsRoB(robChunk.BlockNumber, chainID, &common.PayloadTxs{ Txs: ethOtxs[chainID], }) + require.NoError(t, err) } else { err = backend.arcadia.db.SetPayloadTxsRoB(robChunk.BlockNumber, chainID, &common.PayloadTxs{ Txs: ethOtxs[chainID], }) + require.NoError(t, err) + + payloadResp := common.GetPayloadResponse{ + Transactions: ethOtxs[chainID], + } + err = backend.arcadia.db.SetPayloadResp(chainID, robChunk.BlockNumber, &payloadResp) + require.NoError(t, err) } - require.NoError(t, err) + payload := &common.GetPayloadRequest{ ChainID: robChunk.ChainID, BlockNumber: robChunk.BlockNumber, } chainIDu64 := binary.LittleEndian.Uint64(rollup.Namespace) chainIDstr := hexutil.EncodeBig(big.NewInt(int64(chainIDu64))) + if backend.useRedis { reqRollup, err := backend.redis.GetRegisterRollup(epoch, chainIDstr) require.NoError(t, err) t.Log(reqRollup) } + payloadBytes, err := json.Marshal(payload) require.NoError(t, err) payloadHash, _ := common.Sha256HashPayload(payloadBytes) sig := bls.Sign(mockSecretKey, payloadHash[:]) sigBytes := sig.Bytes() sigStr := hex.EncodeToString(sigBytes[:]) + rrCode, rrBytes := processBlockRequest(backend, payload, "0x"+sigStr) require.Equal(t, http.StatusOK, rrCode) + respRoB := new(common.GetPayloadResponse) err = json.Unmarshal(rrBytes, respRoB) require.NoError(t, err) + robTxs := respRoB.Transactions - require.NotNil(t, len(robTxs)) + require.Equal(t, len(robTxs), 1) }) for _, conn := range conns { err := conn.Close() require.NoError(t, err) } + + t.Log("*** Finished Overall Flow param disable_redis [" + strconv.FormatBool(tc.disableRedis) + "] ***") } } @@ -6277,6 +6570,7 @@ func TestToBNonceState(t *testing.T) { for _, acct := range testAccounts { t.Logf("acct(%s) info: nonce(%d), balance(%d)\n", acct.Address.Hex(), acct.Nonce, acct.Balance.Int64()) } + backend := newTestBackend(t) t.Run("Run valid base case, just ToB", func(t *testing.T) { @@ -6292,10 +6586,10 @@ func TestToBNonceState(t *testing.T) { // constructing ToB bundleTxs := map[string]ethtypes.Transactions{ originChainIDStr: { - CreateEthTransfer(t, originChainID, testAccounts[0].PrivateKey, testAccounts[1].Address, 100, testAccounts[0].Nonce, 21000, nil), + CreateEthTransfer(t, originChainID, testAccounts[0].PrivateKey, testAccounts[1].Address, 100, 1, 21000, nil), }, remoteChainIDStr: { - CreateEthTransfer(t, remoteChainID, testAccounts[1].PrivateKey, testAccounts[0].Address, 100, testAccounts[1].Nonce, 21000, nil), + CreateEthTransfer(t, remoteChainID, testAccounts[1].PrivateKey, testAccounts[0].Address, 100, 0, 21000, nil), }, } seqTx := CreateHypersdkBundleTx(t, testSeqChainID, bundleTxs) @@ -6322,7 +6616,7 @@ func TestToBNonceState(t *testing.T) { redis := backend.redis redis.SetSizeTracker(backend.arcadia.sizeTracker) require.Equal(t, backend.arcadia.chunkManager.ToBNonce(), uint64(0)) - backend.simulator.EXPECT().GetBlockNumber([]string{originChainIDStr, remoteChainIDStr}).Return(blockNumbers, nil) + // backend.simulator.EXPECT().GetBlockNumber([]string{originChainIDStr, remoteChainIDStr}).Return(blockNumbers, nil) backend.SetupRegisteredRollups(epoch, originChainID) backend.SetupRegisteredRollups(epoch, remoteChainID) @@ -6331,27 +6625,62 @@ func TestToBNonceState(t *testing.T) { // shared calls for both chunks backend.seqcli.EXPECT().Parser().Return(chainParser) + id := ids.GenerateTestID() + + // send in rob chunk for origin chain id + // needed for ToB to be accepted + robOpts1 := &CreateTestBlockSubmissionOpts{ + Epoch: epoch, + OriginChainID: *originChainID, + RemoteChainID: *remoteChainID, + SeqChainID: testSeqChainID, + BuilderPubkey: *testBuilderPublicKey, + BuilderSecretkey: *testBuilderSecretKey, + RoBBlockNumber: blockNumbers[originChainIDStr] + 1, + RoBChainID: originChainID, + ChunkID: id, + Txs: ethtypes.Transactions{ + CreateEthTransfer(t, originChainID, testAccounts[0].PrivateKey, testAccounts[1].Address, 20, testAccounts[0].Nonce, 21000, nil), + }, + IsToB: false, + } + //rob1 := backend.submitRoBChunk(t, robOpts1, chainParser) + rob1 := backend.submitRoBChunk(t, robOpts1, chainParser).Chunk + + // send in rob chunk for remote chain id + // needed for ToB to be accepted + require.NoError(t, err) + robOpts2 := &CreateTestBlockSubmissionOpts{ + Epoch: epoch, + OriginChainID: *remoteChainID, + RemoteChainID: *originChainID, + SeqChainID: testSeqChainID, + BuilderPubkey: *testBuilderPublicKey, + BuilderSecretkey: *testBuilderSecretKey, + RoBBlockNumber: blockNumbers[remoteChainIDStr] + 1, + RoBChainID: remoteChainID, + ChunkID: id, + Txs: ethtypes.Transactions{ + CreateEthTransfer(t, remoteChainID, testAccounts[0].PrivateKey, testAccounts[1].Address, 20, testAccounts[0].Nonce, 21000, nil), + }, + IsToB: false, + } + rob2 := backend.submitRoBChunk(t, robOpts2, chainParser).Chunk + + backend.resetSeqClientMockExpectations(t) + backend.simulator.EXPECT().GetBlockNumber([]string{originChainIDStr, remoteChainIDStr}).Return(blockNumbers, nil) + t.Log("========setup & send TOB============") tobTxs, err := common.CollectTxsFromChunks([]*common.ArcadiaChunk{&tobReq.Chunk}) require.NoError(t, err) common.DisplayEthTxs(tobTxs) - allTxs, err := common.CollectTxsFromChunks([]*common.ArcadiaChunk{&tobReq.Chunk}) + + allTxs, err := common.CollectTxsFromChunks([]*common.ArcadiaChunk{&rob1, &rob2, &tobReq.Chunk}) require.NoError(t, err) common.DisplayEthTxs(allTxs) - // expectations for the first tob - for domain, expectedTxs := range tobTxs { - matchTxs := func(txs ethtypes.Transactions) bool { - if len(txs) != len(expectedTxs) { - return false - } - for i, tx := range txs { - if tx.Hash().Hex() != expectedTxs[i].Hash().Hex() { - return false - } - } - return true - } + // expectations for the first tob + for domain, expectedTxs := range allTxs { fmt.Printf("expected txs for domain: %s\n", domain) fmt.Printf("[") for _, tx := range expectedTxs { @@ -6363,8 +6692,9 @@ func TestToBNonceState(t *testing.T) { require.NoError(t, err) nonces := CollectNoncesFromEthAccounts(relatedAccounts) balances := CollectBalancesFromEthAccounts(relatedAccounts) - backend.simulator.EXPECT().GetNonces(domain, mock.MatchedBy(matchTxs), uint64ToHexString(blockNumbers[domain])).Return(nonces, nil) - backend.simulator.EXPECT().GetBalances(domain, mock.MatchedBy(matchTxs), uint64ToHexString(blockNumbers[domain])).Return(balances, nil) + + backend.simulator.EXPECT().GetNonces(domain, mock.MatchedBy(ExpectedMatchTxs(expectedTxs)), uint64ToHexString(blockNumbers[domain])).Return(nonces, nil) + backend.simulator.EXPECT().GetBalances(domain, mock.MatchedBy(ExpectedMatchTxs(expectedTxs)), uint64ToHexString(blockNumbers[domain])).Return(balances, nil) // simulation results, only the txs from this ToB will be simulated and the txs from RoB will be filtered out callBundleRes := make([]flashbotsrpc.FlashbotsCallBundleResult, 0, len(expectedTxs)) @@ -6391,7 +6721,7 @@ func TestToBNonceState(t *testing.T) { require.Equal(t, http.StatusOK, rrCode) cmTxs, cmHeights, err := backend.arcadia.chunkManager.Txs() require.NoError(t, err) - TxsTheSame(t, bundleTxs, cmTxs) + TxsTheSame(t, allTxs, cmTxs) require.Equal(t, blockNumbers, cmHeights) for _, bundle := range tobReq.Chunk.ToB.GetBundles() { @@ -6400,9 +6730,10 @@ func TestToBNonceState(t *testing.T) { require.Equal(t, common.BundleAccepted, status) } }) + arcadia2, err := NewArcadiaAPI(*backend.currOpts) require.NoError(t, err) - require.Equal(t, arcadia2.chunkManager.ToBNonce(), uint64(1)) + require.Equal(t, uint64(1), arcadia2.chunkManager.ToBNonce()) } func TestConcurrentSubmitReqNGetPayload(t *testing.T) { diff --git a/services/api/testing_utils.go b/services/api/testing_utils.go index c4284185..7ba21f4d 100644 --- a/services/api/testing_utils.go +++ b/services/api/testing_utils.go @@ -149,7 +149,6 @@ func CreateTestChunkSubmission( blockReq.Pubkey = builderPubkeyBytes[:] require.NoError(t, err) - // blockReq.Signature = &bls.Signature{} isToB := len(opts.ChainIDs) > 1 var chunk common.ArcadiaChunk var payloadHash [32]byte @@ -562,6 +561,15 @@ func TxsTheSame(t *testing.T, expected map[string]ethtypes.Transactions, actual } } +func TxsTheSameUnordered(t *testing.T, expected map[string]ethtypes.Transactions, actual map[string]ethtypes.Transactions) { + require.Equal(t, len(expected), len(actual)) + for domain, expectedTxs := range expected { + actualTxs := actual[domain] + require.NotNil(t, actualTxs) + require.True(t, common.TxsHashUnorderedMatch(expectedTxs, actualTxs)) + } +} + func CreateTestEthTransaction(nonce uint64, value big.Int, gasLimit uint64, gasPrice big.Int, data []byte) *ethtypes.Transaction { toAddress := ethcommon.HexToAddress(TestAddressValue) _, err := crypto.HexToECDSA(TestPrivateKeyValue) @@ -666,3 +674,18 @@ func waitForServerReady(addr string, timeout time.Duration) error { } return fmt.Errorf("server not ready after %v", timeout) } + +// ExpectedMatchTxs creates a matcher functor that can be used in mock testing for tx matching +func ExpectedMatchTxs(expectedTxs ethtypes.Transactions) func(txs ethtypes.Transactions) bool { + return func(txs ethtypes.Transactions) bool { + if len(txs) != len(expectedTxs) { + return false + } + for i, tx := range txs { + if tx.Hash().Hex() != expectedTxs[i].Hash().Hex() { + return false + } + } + return true + } +} diff --git a/services/api/tx_table.go b/services/api/tx_table.go index 58a2c10a..dece3471 100644 --- a/services/api/tx_table.go +++ b/services/api/tx_table.go @@ -31,7 +31,7 @@ func (t *TxTable) AddBundledTxns(txns []*ethtypes.Transaction) error { for _, tx := range txns { sender, err := common.ExtractSender(tx) if err != nil { - return fmt.Errorf("unable to extract sender from tx: %s err: %w", tx.Hash().Hex(), err) + return fmt.Errorf("unable to extract sender from tx: [%s], sender: [%s], err: %w", tx.Hash().Hex(), sender, err) } // nonce check @@ -44,7 +44,7 @@ func (t *TxTable) AddBundledTxns(txns []*ethtypes.Transaction) error { nonce = nonce + uint64(len(t.table[sender])) + uint64(len(pending[sender])) // nonce not consecutive if tx.Nonce() != nonce { - return fmt.Errorf("nonce not consecutive: wanted: %d, actual: %d", nonce, tx.Nonce()) + return fmt.Errorf("nonce not consecutive: wanted: [%d], actual: [%d], for tx [%s] with sender [%s]", nonce, tx.Nonce(), tx.Hash().Hex(), sender) } // balance check, the balance consumed can be negative for consuming or positive for receiving funds(transfer) @@ -59,14 +59,14 @@ func (t *TxTable) AddBundledTxns(txns []*ethtypes.Transaction) error { // Intrinsic Gas Calculation: https://github.com/wolflo/evm-opcodes/blob/main/gas.md gas := intrinsicGas(tx) if tx.Gas() < uint64(gas) { - return fmt.Errorf("provided insufficient gas, want: %d, provided: %d", gas, tx.Gas()) + return fmt.Errorf("provided insufficient gas, want: %d, provided: %d, for tx [%s] with sender [%s]", gas, tx.Gas(), tx.Hash().Hex(), sender) } // accumulates txValue + gas to consumed consumed = consumed.Add(consumed, txValue) consumed = consumed.Add(consumed, big.NewInt(int64(gas))) - // consumed = consumed.Add(consumed, big.NewInt(int64(21000))) balanceConsumed[sender] = consumed.Neg(consumed) + if tx.Value().Cmp(big.NewInt(0)) != 0 { recipient := tx.To().Hex() if _, ok := balanceConsumed[recipient]; !ok { @@ -98,6 +98,7 @@ func (t *TxTable) AddBundledTxns(txns []*ethtypes.Transaction) error { balanceAfterConsuming := big.NewInt(0) balanceAfterConsuming = balanceAfterConsuming.Add(balanceAfterConsuming, consumed) balanceAfterConsuming = balanceAfterConsuming.Add(balanceAfterConsuming, remainBalance) + // no enough balance to cover tx fee + value if balanceAfterConsuming.Cmp(big.NewInt(0)) == -1 { return fmt.Errorf("remaining balance for sender(%s) cannot cover tx execution(value + gas)", sender) diff --git a/simulator/mocks/mock_IBlockSimRateLimiter.go b/simulator/mocks/mock_IBlockSimRateLimiter.go index 227fbe28..4b7b1539 100644 --- a/simulator/mocks/mock_IBlockSimRateLimiter.go +++ b/simulator/mocks/mock_IBlockSimRateLimiter.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.50.0. DO NOT EDIT. +// Code generated by mockery v2.39.0. DO NOT EDIT. package mocks @@ -30,7 +30,7 @@ func (_m *MockIBlockSimRateLimiter) EXPECT() *MockIBlockSimRateLimiter_Expecter return &MockIBlockSimRateLimiter_Expecter{mock: &_m.Mock} } -// CurrentCounter provides a mock function with no fields +// CurrentCounter provides a mock function with given fields: func (_m *MockIBlockSimRateLimiter) CurrentCounter() int64 { ret := _m.Called()