diff --git a/app/priority_nonce.go b/app/priority_nonce.go index a7c604de..e2874e97 100644 --- a/app/priority_nonce.go +++ b/app/priority_nonce.go @@ -2,10 +2,13 @@ package app import ( "context" + "sync" + "fmt" "math" "github.com/huandu/skiplist" + "github.com/pkg/errors" sdk "github.com/cosmos/cosmos-sdk/types" "github.com/cosmos/cosmos-sdk/types/mempool" @@ -14,11 +17,15 @@ import ( evmtypes "github.com/evmos/ethermint/x/evm/types" ) -const MAX_TXS_PRE_SENDER_IN_MEMPOOL int = 10 +const MAX_TXS_PRE_SENDER_IN_MEMPOOL int = 16 var ( _ mempool.Mempool = (*PriorityNonceMempool)(nil) _ mempool.Iterator = (*PriorityNonceIterator)(nil) + + errMempoolTxGasPriceTooLow = errors.New("gas price is too low") + errMempoolTooManyTxs = errors.New("tx sender has too many txs in mempool") + errMempoolIsFull = errors.New("mempool is full") ) // PriorityNonceMempool is a mempool implementation that stores txs @@ -29,14 +36,19 @@ var ( // priority to other sender txs and must be partially ordered by both sender-nonce // and priority. type PriorityNonceMempool struct { - priorityIndex *skiplist.SkipList - priorityCounts map[int64]int - senderIndices map[string]*skiplist.SkipList - scores map[txMeta]txMeta - onRead func(tx sdk.Tx) - txReplacement func(op, np int64, oTx, nTx sdk.Tx) bool - maxTx int + priorityIndex *skiplist.SkipList + priorityCounts map[int64]int + senderIndices map[string]*skiplist.SkipList + scores map[txMeta]txMeta + onRead func(tx sdk.Tx) + txReplacement func(op, np int64, oTx, nTx sdk.Tx) bool + maxTx int + + senderTxCntLock sync.RWMutex counterBySender map[string]int + txRecord map[txMeta]struct{} + + txReplacedCallback func(ctx context.Context, oldTx, newTx sdk.Tx) } type PriorityNonceIterator struct { @@ -123,6 +135,12 @@ func PriorityNonceWithMaxTx(maxTx int) PriorityNonceMempoolOption { } } +func PriorityNonceWithTxReplacedCallback(cb func(ctx context.Context, oldTx, newTx sdk.Tx)) PriorityNonceMempoolOption { + return func(mp *PriorityNonceMempool) { + mp.txReplacedCallback = cb + } +} + // DefaultPriorityMempool returns a priorityNonceMempool with no options. func DefaultPriorityMempool() mempool.Mempool { return NewPriorityMempool() @@ -137,6 +155,7 @@ func NewPriorityMempool(opts ...PriorityNonceMempoolOption) *PriorityNonceMempoo senderIndices: make(map[string]*skiplist.SkipList), scores: make(map[txMeta]txMeta), counterBySender: make(map[string]int), + txRecord: make(map[txMeta]struct{}), } for _, opt := range opts { @@ -169,67 +188,37 @@ func (mp *PriorityNonceMempool) NextSenderTx(sender string) sdk.Tx { // Inserting a duplicate tx with a different priority overwrites the existing tx, // changing the total order of the mempool. func (mp *PriorityNonceMempool) Insert(ctx context.Context, tx sdk.Tx) error { - if mp.maxTx > 0 && mp.CountTx() >= mp.maxTx { - return mempool.ErrMempoolTxMaxCapacity - } else if mp.maxTx < 0 { + // if mp.maxTx > 0 && mp.CountTx() >= mp.maxTx { + // return mempool.ErrMempoolTxMaxCapacity + // } else + if mp.maxTx < 0 { return nil } - sigs, err := tx.(signing.SigVerifiableTx).GetSignaturesV2() + sdkContext := sdk.UnwrapSDKContext(ctx) + priority := sdkContext.Priority() + txInfo, err := extractTxInfo(tx) if err != nil { return err } - sdkContext := sdk.UnwrapSDKContext(ctx) - priority := sdkContext.Priority() - var sender string - var nonce uint64 - - if len(sigs) == 0 { - msgs := tx.GetMsgs() - if len(msgs) != 1 { - return fmt.Errorf("tx must have at least one signer") - } - msgEthTx, ok := msgs[0].(*evmtypes.MsgEthereumTx) - if !ok { - return fmt.Errorf("tx must have at least one signer") - } - ethTx := msgEthTx.AsTransaction() - signer := gethtypes.NewEIP2930Signer(ethTx.ChainId()) - ethSender, err := signer.Sender(ethTx) - if err != nil { - return fmt.Errorf("tx must have at least one signer") - } - sender = sdk.AccAddress(ethSender.Bytes()).String() - nonce = ethTx.Nonce() - } else { - sig := sigs[0] - sender = sdk.AccAddress(sig.PubKey.Address()).String() - nonce = sig.Sequence + if !mp.canInsert(txInfo.sender) { + return errors.Wrapf(errMempoolTooManyTxs, "sender %s has too many txs in mempool", txInfo.sender) } - if _, exists := mp.counterBySender[sender]; !exists { - mp.counterBySender[sender] = 1 - } else { - if mp.counterBySender[sender] < MAX_TXS_PRE_SENDER_IN_MEMPOOL { - mp.counterBySender[sender] += 1 - } else { - return fmt.Errorf("tx sender has too many txs in mempool") - } - } - - key := txMeta{nonce: nonce, priority: priority, sender: sender} - - senderIndex, ok := mp.senderIndices[sender] + // init sender index if not exists + senderIndex, ok := mp.senderIndices[txInfo.sender] if !ok { senderIndex = skiplist.New(skiplist.LessThanFunc(func(a, b any) int { return skiplist.Uint64.Compare(b.(txMeta).nonce, a.(txMeta).nonce) })) // initialize sender index if not found - mp.senderIndices[sender] = senderIndex + mp.senderIndices[txInfo.sender] = senderIndex } + newKey := txMeta{nonce: txInfo.nonce, priority: priority, sender: txInfo.sender} + // Since mp.priorityIndex is scored by priority, then sender, then nonce, a // changed priority will create a new key, so we must remove the old key and // re-insert it to avoid having the same tx with different priorityIndex indexed @@ -237,35 +226,145 @@ func (mp *PriorityNonceMempool) Insert(ctx context.Context, tx sdk.Tx) error { // // This O(log n) remove operation is rare and only happens when a tx's priority // changes. - sk := txMeta{nonce: nonce, sender: sender} - if oldScore, txExists := mp.scores[sk]; txExists { - if mp.txReplacement != nil && !mp.txReplacement(oldScore.priority, priority, senderIndex.Get(key).Value.(sdk.Tx), tx) { - return fmt.Errorf( - "tx doesn't fit the replacement rule, oldPriority: %v, newPriority: %v, oldTx: %v, newTx: %v", - oldScore.priority, - priority, - senderIndex.Get(key).Value.(sdk.Tx), - tx, - ) - } - mp.priorityIndex.Remove(txMeta{ - nonce: nonce, - sender: sender, - priority: oldScore.priority, - weight: oldScore.weight, - }) - mp.priorityCounts[oldScore.priority]-- + sk := txMeta{nonce: txInfo.nonce, sender: txInfo.sender} + if oldScore, txExists := mp.scores[sk]; txExists { + oldTx := senderIndex.Get(newKey).Value.(sdk.Tx) + return mp.doTxReplace(ctx, newKey, oldScore, oldTx, tx) + } else { + mempoolSize := mp.CountTx() + if mempoolSize >= mp.maxTx { + lowestPriority := mp.GetLowestPriority() + // find one to replace + if lowestPriority > 0 && priority <= lowestPriority { + return errors.Wrapf(errMempoolTxGasPriceTooLow, "tx with priority %d is too low, current lowest priority is %d", priority, lowestPriority) + } + + var maxIndexSize int + var lowerPriority int64 = math.MaxInt64 + var selectedElement *skiplist.Element + for sender, index := range mp.senderIndices { + indexSize := index.Len() + if sender == txInfo.sender { + continue + } + + if indexSize > 1 { + tail := index.Back() + if tail != nil { + tailKey := tail.Key().(txMeta) + if tailKey.priority < lowerPriority { + lowerPriority = tailKey.priority + maxIndexSize = indexSize + selectedElement = tail + } else if tailKey.priority == lowerPriority { + if indexSize > maxIndexSize { + maxIndexSize = indexSize + selectedElement = tail + } + } + } + } + } + + if selectedElement != nil { + key := selectedElement.Key().(txMeta) + replacedTx, _ := mp.doRemove(key, true) + + // insert new tx + mp.doInsert(newKey, tx, true) + + if mp.txReplacedCallback != nil && replacedTx != nil { + mp.txReplacedCallback(ctx, replacedTx, tx) + } + } else { + // not found any index more than 1 except sender's index + // We do not replace the sender's only tx in the mempool + return errMempoolIsFull + } + } else { + mp.doInsert(newKey, tx, true) + } + return nil + } +} + +func (mp *PriorityNonceMempool) doInsert(newKey txMeta, tx sdk.Tx, incrCnt bool) { + senderIndex, ok := mp.senderIndices[newKey.sender] + if !ok { + senderIndex = skiplist.New(skiplist.LessThanFunc(func(a, b any) int { + return skiplist.Uint64.Compare(b.(txMeta).nonce, a.(txMeta).nonce) + })) + + // initialize sender index if not found + mp.senderIndices[newKey.sender] = senderIndex } - mp.priorityCounts[priority]++ + mp.priorityCounts[newKey.priority]++ + newKey.senderElement = senderIndex.Set(newKey, tx) - // Since senderIndex is scored by nonce, a changed priority will overwrite the - // existing key. - key.senderElement = senderIndex.Set(key, tx) + mp.scores[txMeta{nonce: newKey.nonce, sender: newKey.sender}] = txMeta{priority: newKey.priority} + mp.priorityIndex.Set(newKey, tx) - mp.scores[sk] = txMeta{priority: priority} - mp.priorityIndex.Set(key, tx) + if incrCnt { + mp.incrSenderTxCnt(newKey.sender, newKey.nonce) + } +} + +func (mp *PriorityNonceMempool) doRemove(oldKey txMeta, decrCnt bool) (sdk.Tx, error) { + scoreKey := txMeta{nonce: oldKey.nonce, sender: oldKey.sender} + score, ok := mp.scores[scoreKey] + if !ok { + return nil, mempool.ErrTxNotFound + } + tk := txMeta{nonce: oldKey.nonce, priority: score.priority, sender: oldKey.sender, weight: score.weight} + + senderTxs, ok := mp.senderIndices[oldKey.sender] + if !ok { + return nil, fmt.Errorf("sender %s not found", oldKey.sender) + } + + mp.priorityIndex.Remove(tk) + removedElem := senderTxs.Remove(tk) + delete(mp.scores, scoreKey) + mp.priorityCounts[score.priority]-- + + if decrCnt { + mp.decrSenderTxCnt(oldKey.sender, oldKey.nonce) + } + + if removedElem == nil { + return nil, mempool.ErrTxNotFound + } + + return removedElem.Value.(sdk.Tx), nil +} + +func (mp *PriorityNonceMempool) doTxReplace(ctx context.Context, newMate, oldMate txMeta, oldTx, newTx sdk.Tx) error { + if mp.txReplacement != nil && !mp.txReplacement(oldMate.priority, newMate.priority, oldTx, newTx) { + return fmt.Errorf( + "tx doesn't fit the replacement rule, oldPriority: %v, newPriority: %v, oldTx: %v, newTx: %v", + oldMate.priority, + newMate.priority, + oldTx, + newTx, + ) + } + + e := mp.priorityIndex.Remove(txMeta{ + nonce: newMate.nonce, + sender: newMate.sender, + priority: oldMate.priority, + weight: oldMate.weight, + }) + replacedTx := e.Value.(sdk.Tx) + mp.priorityCounts[oldMate.priority]-- + + mp.doInsert(newMate, newTx, false) + + if mp.txReplacedCallback != nil && replacedTx != nil { + mp.txReplacedCallback(ctx, replacedTx, newTx) + } return nil } @@ -421,45 +520,23 @@ func (mp *PriorityNonceMempool) CountTx() int { // Remove removes a transaction from the mempool in O(log n) time, returning an // error if unsuccessful. func (mp *PriorityNonceMempool) Remove(tx sdk.Tx) error { - sigs, err := tx.(signing.SigVerifiableTx).GetSignaturesV2() + txInfo, err := extractTxInfo(tx) if err != nil { return err } - var sender string - var nonce uint64 - if len(sigs) == 0 { - msgs := tx.GetMsgs() - if len(msgs) != 1 { - return fmt.Errorf("attempted to remove a tx with no signatures") - } - msgEthTx, ok := msgs[0].(*evmtypes.MsgEthereumTx) - if !ok { - return fmt.Errorf("attempted to remove a tx with no signatures") - } - ethTx := msgEthTx.AsTransaction() - signer := gethtypes.NewEIP2930Signer(ethTx.ChainId()) - ethSender, err := signer.Sender(ethTx) - if err != nil { - return fmt.Errorf("attempted to remove a tx with no signatures") - } - sender = sdk.AccAddress(ethSender.Bytes()).String() - nonce = ethTx.Nonce() - } else { - sig := sigs[0] - sender = sdk.AccAddress(sig.PubKey.Address()).String() - nonce = sig.Sequence - } - scoreKey := txMeta{nonce: nonce, sender: sender} + mp.decrSenderTxCnt(txInfo.sender, txInfo.nonce) + + scoreKey := txMeta{nonce: txInfo.nonce, sender: txInfo.sender} score, ok := mp.scores[scoreKey] if !ok { return mempool.ErrTxNotFound } - tk := txMeta{nonce: nonce, priority: score.priority, sender: sender, weight: score.weight} + tk := txMeta{nonce: txInfo.nonce, priority: score.priority, sender: txInfo.sender, weight: score.weight} - senderTxs, ok := mp.senderIndices[sender] + senderTxs, ok := mp.senderIndices[txInfo.sender] if !ok { - return fmt.Errorf("sender %s not found", sender) + return fmt.Errorf("sender %s not found", txInfo.sender) } mp.priorityIndex.Remove(tk) @@ -467,17 +544,76 @@ func (mp *PriorityNonceMempool) Remove(tx sdk.Tx) error { delete(mp.scores, scoreKey) mp.priorityCounts[score.priority]-- - if _, exists := mp.counterBySender[sender]; exists { - if mp.counterBySender[sender] > 1 { - mp.counterBySender[sender] -= 1 - } else { - delete(mp.counterBySender, sender) + return nil +} + +func (mp *PriorityNonceMempool) GetLowestPriority() int64 { + if mp.priorityIndex.Len() == 0 { + return 0 + } + + min := int64(math.MaxInt64) + for priority, count := range mp.priorityCounts { + if count > 0 { + if priority < min { + min = priority + } } } + return min +} + +func (mp *PriorityNonceMempool) canInsert(sender string) bool { + mp.senderTxCntLock.RLock() + defer mp.senderTxCntLock.RUnlock() + + if _, exists := mp.counterBySender[sender]; exists { + return mp.counterBySender[sender] < MAX_TXS_PRE_SENDER_IN_MEMPOOL + } + + return true +} + +func (mp *PriorityNonceMempool) incrSenderTxCnt(sender string, nonce uint64) error { + mp.senderTxCntLock.Lock() + defer mp.senderTxCntLock.Unlock() + + existsKey := txMeta{nonce: nonce, sender: sender} + if _, exists := mp.txRecord[existsKey]; !exists { + mp.txRecord[existsKey] = struct{}{} + + if _, exists := mp.counterBySender[sender]; !exists { + mp.counterBySender[sender] = 1 + } else { + if mp.counterBySender[sender] < MAX_TXS_PRE_SENDER_IN_MEMPOOL { + mp.counterBySender[sender] += 1 + } else { + return fmt.Errorf("tx sender has too many txs in mempool") + } + } + } return nil } +func (mp *PriorityNonceMempool) decrSenderTxCnt(sender string, nonce uint64) { + mp.senderTxCntLock.Lock() + defer mp.senderTxCntLock.Unlock() + + existsKey := txMeta{nonce: nonce, sender: sender} + if _, exists := mp.txRecord[existsKey]; exists { + delete(mp.txRecord, existsKey) + + if _, exists := mp.counterBySender[sender]; exists { + if mp.counterBySender[sender] > 1 { + mp.counterBySender[sender] -= 1 + } else { + delete(mp.counterBySender, sender) + } + } + } +} + func IsEmpty(mempool mempool.Mempool) error { mp := mempool.(*PriorityNonceMempool) if mp.priorityIndex.Len() != 0 { @@ -508,3 +644,43 @@ func IsEmpty(mempool mempool.Mempool) error { return nil } + +type txInfo struct { + sender string + nonce uint64 +} + +func extractTxInfo(tx sdk.Tx) (*txInfo, error) { + var sender string + var nonce uint64 + + sigs, err := tx.(signing.SigVerifiableTx).GetSignaturesV2() + if err != nil { + return nil, err + } + + if len(sigs) == 0 { + msgs := tx.GetMsgs() + if len(msgs) != 1 { + return nil, fmt.Errorf("tx must have at least one signer") + } + msgEthTx, ok := msgs[0].(*evmtypes.MsgEthereumTx) + if !ok { + return nil, fmt.Errorf("tx must have at least one signer") + } + ethTx := msgEthTx.AsTransaction() + signer := gethtypes.NewEIP2930Signer(ethTx.ChainId()) + ethSender, err := signer.Sender(ethTx) + if err != nil { + return nil, fmt.Errorf("tx must have at least one signer") + } + sender = sdk.AccAddress(ethSender.Bytes()).String() + nonce = ethTx.Nonce() + } else { + sig := sigs[0] + sender = sdk.AccAddress(sig.PubKey.Address()).String() + nonce = sig.Sequence + } + + return &txInfo{sender: sender, nonce: nonce}, nil +} diff --git a/cmd/0gchaind/app.go b/cmd/0gchaind/app.go index f09ec847..7a6b1fba 100644 --- a/cmd/0gchaind/app.go +++ b/cmd/0gchaind/app.go @@ -1,6 +1,7 @@ package main import ( + "context" "errors" "fmt" "io" @@ -19,6 +20,7 @@ import ( snapshottypes "github.com/cosmos/cosmos-sdk/snapshots/types" "github.com/cosmos/cosmos-sdk/store" sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/cosmos-sdk/x/auth/signing" "github.com/cosmos/cosmos-sdk/x/crisis" ethermintflags "github.com/evmos/ethermint/server/flags" "github.com/spf13/cast" @@ -26,6 +28,8 @@ import ( "github.com/0glabs/0g-chain/app" "github.com/0glabs/0g-chain/app/params" + gethtypes "github.com/ethereum/go-ethereum/core/types" + evmtypes "github.com/evmos/ethermint/x/evm/types" ) const ( @@ -107,8 +111,6 @@ func (ac appCreator) newApp( skipLoadLatest = cast.ToBool(appOpts.Get(flagSkipLoadLatest)) } - mempool := app.NewPriorityMempool() - bApp := app.NewBaseApp(logger, db, ac.encodingConfig, baseapp.SetPruning(pruningOpts), baseapp.SetMinGasPrices(strings.Replace(cast.ToString(appOpts.Get(server.FlagMinGasPrices)), ";", ",", -1)), @@ -123,8 +125,17 @@ func (ac appCreator) newApp( baseapp.SetIAVLDisableFastNode(cast.ToBool(iavlDisableFastNode)), baseapp.SetIAVLLazyLoading(cast.ToBool(appOpts.Get(server.FlagIAVLLazyLoading))), baseapp.SetChainID(chainID), - baseapp.SetMempool(mempool), + baseapp.SetTxInfoExtracter(extractTxInfo), ) + + mempool := app.NewPriorityMempool( + app.PriorityNonceWithMaxTx(fixMempoolSize(appOpts)), + app.PriorityNonceWithTxReplacedCallback(func(ctx context.Context, oldTx, newTx sdk.Tx) { + bApp.RegisterMempoolTxReplacedEvent(ctx, oldTx, newTx) + }), + ) + bApp.SetMempool(mempool) + bApp.SetTxEncoder(ac.encodingConfig.TxConfig.TxEncoder()) abciProposalHandler := app.NewDefaultProposalHandler(mempool, bApp) bApp.SetPrepareProposal(abciProposalHandler.PrepareProposalHandler()) @@ -199,3 +210,72 @@ func accAddressesFromBech32(addresses ...string) ([]sdk.AccAddress, error) { } return decodedAddresses, nil } + +var ErrMustHaveSigner error = errors.New("tx must have at least one signer") + +func extractTxInfo(ctx sdk.Context, tx sdk.Tx) (*sdk.TxInfo, error) { + sigs, err := tx.(signing.SigVerifiableTx).GetSignaturesV2() + if err != nil { + return nil, err + } + + var sender string + var nonce uint64 + var gasPrice uint64 + var gasLimit uint64 + var txType int32 + + if len(sigs) == 0 { + txType = 1 + msgs := tx.GetMsgs() + if len(msgs) != 1 { + return nil, ErrMustHaveSigner + } + msgEthTx, ok := msgs[0].(*evmtypes.MsgEthereumTx) + if !ok { + return nil, ErrMustHaveSigner + } + ethTx := msgEthTx.AsTransaction() + signer := gethtypes.NewEIP2930Signer(ethTx.ChainId()) + ethSender, err := signer.Sender(ethTx) + if err != nil { + return nil, ErrMustHaveSigner + } + sender = sdk.AccAddress(ethSender.Bytes()).String() + nonce = ethTx.Nonce() + gasPrice = ethTx.GasPrice().Uint64() + gasLimit = ethTx.Gas() + } else { + sig := sigs[0] + sender = sdk.AccAddress(sig.PubKey.Address()).String() + nonce = sig.Sequence + } + + return &sdk.TxInfo{ + SignerAddress: sender, + Nonce: nonce, + GasLimit: gasLimit, + GasPrice: gasPrice, + TxType: txType, + }, nil +} + +func fixMempoolSize(appOpts servertypes.AppOptions) int { + val1 := appOpts.Get("mempool.size") + val2 := appOpts.Get(server.FlagMempoolMaxTxs) + + if val1 != nil && val2 != nil { + size1 := cast.ToInt(val1) + size2 := cast.ToInt(val2) + if size1 != size2 { + panic("the value of mempool.size and mempool.max-txs are different") + } + return size1 + } else if val1 == nil && val2 == nil { + panic("not found mempool size in config") + } else if val1 == nil { + return cast.ToInt(val2) + } else { //if val2 == nil { + return cast.ToInt(val1) + } +} diff --git a/go.mod b/go.mod index 15434cb9..9ed2f7f0 100644 --- a/go.mod +++ b/go.mod @@ -250,7 +250,7 @@ replace ( // TODO: Tag before release github.com/ethereum/go-ethereum => github.com/evmos/go-ethereum v1.10.26-evmos-rc2 // Use ethermint fork that respects min-gas-price with NoBaseFee true and london enabled, and includes eip712 support - github.com/evmos/ethermint => github.com/0glabs/ethermint v0.21.0-0g.v3.1.12 + github.com/evmos/ethermint => github.com/0glabs/ethermint v0.21.0-0g.v3.1.14 // See https://github.com/cosmos/cosmos-sdk/pull/10401, https://github.com/cosmos/cosmos-sdk/commit/0592ba6158cd0bf49d894be1cef4faeec59e8320 github.com/gin-gonic/gin => github.com/gin-gonic/gin v1.9.0 // Downgraded to avoid bugs in following commits which causes "version does not exist" errors diff --git a/go.sum b/go.sum index f0821f5f..38cacc94 100644 --- a/go.sum +++ b/go.sum @@ -213,8 +213,8 @@ github.com/0glabs/cometbft v0.37.9-0glabs.1 h1:KQJG17Y21suKP3QNICLto4b5Ak73XbSmK github.com/0glabs/cometbft v0.37.9-0glabs.1/go.mod h1:j0Q3RqrCd+cztWCugs3obbzC4NyHGBPZZjtm/fWV00I= github.com/0glabs/cosmos-sdk v0.47.10-0glabs.10 h1:NJp0RwczHBO4EvrQdDxxftHOgUDBtNh7M/vpaG7wFtQ= github.com/0glabs/cosmos-sdk v0.47.10-0glabs.10/go.mod h1:KskIVnhXTFqrw7CDccMvx7To5KzUsOomIsQV7sPGOog= -github.com/0glabs/ethermint v0.21.0-0g.v3.1.12 h1:IRVTFhDEH2J5w8ywQW7obXQxYhJYib70SNgKqLOXikU= -github.com/0glabs/ethermint v0.21.0-0g.v3.1.12/go.mod h1:6e/gOcDLhvlDWK3JLJVBgki0gD6H4E1eG7l9byocgWA= +github.com/0glabs/ethermint v0.21.0-0g.v3.1.14 h1:Ns1TNEwcOScVt8qlAYK3tZ5Xf0o0v+7IRZCFb1BL2TY= +github.com/0glabs/ethermint v0.21.0-0g.v3.1.14/go.mod h1:6e/gOcDLhvlDWK3JLJVBgki0gD6H4E1eG7l9byocgWA= github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMbk2FiG/kXiLl8BRyzTWDw7gX/Hz7Dd5eDMs= github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4= github.com/Azure/azure-sdk-for-go/sdk/azcore v0.21.1/go.mod h1:fBF9PQNqB8scdgpZ3ufzaLntG0AG7C1WjPMsiFOmfHM=