From 13d32d5310d9f8b6246f6021a44c2bd642ebb901 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Nowosielski?= Date: Mon, 9 Oct 2023 17:00:58 +0200 Subject: [PATCH 1/7] porting builder changes - code compiles --- common/big.go | 6 + core/error.go | 4 + core/sbundle_sim.go | 149 ++ core/state/multi_tx_snapshot.go | 692 +++++++++ core/state/statedb.go | 39 + core/txpool/sbundle_pool.go | 250 ++++ core/txpool/txpool.go | 419 +++++- core/types/sbundle.go | 116 ++ core/types/transaction.go | 270 +++- eth/backend.go | 4 + miner/algo_common.go | 352 +++++ miner/algo_greedy.go | 114 ++ miner/algo_greedy_buckets.go | 228 +++ miner/algo_greedy_buckets_multisnap.go | 244 ++++ miner/algo_greedy_multisnap.go | 137 ++ miner/bundle_cache.go | 116 ++ miner/env_changes.go | 427 ++++++ miner/environment_diff.go | 418 ++++++ miner/miner.go | 112 +- miner/multi_worker.go | 209 +++ miner/payload_building.go | 73 +- miner/{test_backend.go => test_backend.gone} | 11 +- miner/worker.go | 1366 +++++++++++++----- 23 files changed, 5228 insertions(+), 528 deletions(-) create mode 100644 core/sbundle_sim.go create mode 100644 core/state/multi_tx_snapshot.go create mode 100644 core/txpool/sbundle_pool.go create mode 100644 core/types/sbundle.go create mode 100644 miner/algo_common.go create mode 100644 miner/algo_greedy.go create mode 100644 miner/algo_greedy_buckets.go create mode 100644 miner/algo_greedy_buckets_multisnap.go create mode 100644 miner/algo_greedy_multisnap.go create mode 100644 miner/bundle_cache.go create mode 100644 miner/env_changes.go create mode 100644 miner/environment_diff.go create mode 100644 miner/multi_worker.go rename miner/{test_backend.go => test_backend.gone} (99%) diff --git a/common/big.go b/common/big.go index 65d4377bf7..713d133b77 100644 --- a/common/big.go +++ b/common/big.go @@ -25,6 +25,12 @@ var ( Big3 = big.NewInt(3) Big0 = big.NewInt(0) Big32 = big.NewInt(32) + Big100 = big.NewInt(100) Big256 = big.NewInt(256) Big257 = big.NewInt(257) ) + +func PercentOf(val *big.Int, percent int) *big.Int { + res := new(big.Int).Mul(val, big.NewInt(int64(percent))) + return new(big.Int).Div(res, Big100) +} diff --git a/core/error.go b/core/error.go index 872ba8d365..87d57fcf70 100644 --- a/core/error.go +++ b/core/error.go @@ -100,4 +100,8 @@ var ( // ErrSenderNoEOA is returned if the sender of a transaction is a contract. ErrSenderNoEOA = errors.New("sender not an eoa") + + // ErrNegativeValue is a sanity error to ensure no one is able to specify a + // transaction with a negative value. + ErrNegativeValue = errors.New("negative value") ) diff --git a/core/sbundle_sim.go b/core/sbundle_sim.go new file mode 100644 index 0000000000..871ec61506 --- /dev/null +++ b/core/sbundle_sim.go @@ -0,0 +1,149 @@ +package core + +import ( + "errors" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/params" +) + +var ( + ErrInvalidInclusion = errors.New("invalid inclusion") + + ErrTxFailed = errors.New("tx failed") + ErrNegativeProfit = errors.New("negative profit") + ErrInvalidBundle = errors.New("invalid bundle") + + SbundlePayoutMaxCostInt uint64 = 30_000 + SbundlePayoutMaxCost = big.NewInt(30_000) +) + +type SimBundleResult struct { + TotalProfit *big.Int + RefundableValue *big.Int + GasUsed uint64 + MevGasPrice *big.Int + BodyLogs []SimBundleBodyLogs +} + +type SimBundleBodyLogs struct { + TxLogs []*types.Log `json:"txLogs,omitempty"` + BundleLogs []SimBundleBodyLogs `json:"bundleLogs,omitempty"` +} + +func NewSimBundleResult() SimBundleResult { + return SimBundleResult{ + TotalProfit: big.NewInt(0), + RefundableValue: big.NewInt(0), + GasUsed: 0, + MevGasPrice: big.NewInt(0), + BodyLogs: nil, + } +} + +// SimBundle simulates a bundle and returns the result +// Arguments are the same as in ApplyTransaction with the same change semantics: +// - statedb is modified +// - header is not modified +// - gp is modified +// - usedGas is modified (by txs that were applied) +// Payout transactions will not be applied to the state. +// GasUsed in return will include the gas that might be used by the payout txs. +func SimBundle(config *params.ChainConfig, bc *BlockChain, author *common.Address, gp *GasPool, statedb *state.StateDB, header *types.Header, b *types.SBundle, txIdx int, usedGas *uint64, cfg vm.Config, logs bool) (SimBundleResult, error) { + res := NewSimBundleResult() + + currBlock := header.Number.Uint64() + if currBlock < b.Inclusion.BlockNumber || currBlock > b.Inclusion.MaxBlockNumber { + return res, ErrInvalidInclusion + } + + // extract constraints into convenient format + refundIdx := make([]bool, len(b.Body)) + refundPercents := make([]int, len(b.Body)) + for _, el := range b.Validity.Refund { + refundIdx[el.BodyIdx] = true + refundPercents[el.BodyIdx] = el.Percent + } + + var ( + coinbaseDelta = new(big.Int) + coinbaseBefore *big.Int + ) + for i, el := range b.Body { + coinbaseDelta.Set(common.Big0) + coinbaseBefore = statedb.GetBalance(header.Coinbase) + + if el.Tx != nil { + statedb.SetTxContext(el.Tx.Hash(), txIdx) + txIdx++ + receipt, err := ApplyTransaction(config, bc, author, gp, statedb, header, el.Tx, usedGas, cfg, nil) + if err != nil { + return res, err + } + if receipt.Status != types.ReceiptStatusSuccessful && !el.CanRevert { + return res, ErrTxFailed + } + res.GasUsed += receipt.GasUsed + if logs { + res.BodyLogs = append(res.BodyLogs, SimBundleBodyLogs{TxLogs: receipt.Logs}) + } + } else if el.Bundle != nil { + innerRes, err := SimBundle(config, bc, author, gp, statedb, header, el.Bundle, txIdx, usedGas, cfg, logs) + if err != nil { + return res, err + } + res.GasUsed += innerRes.GasUsed + if logs { + res.BodyLogs = append(res.BodyLogs, SimBundleBodyLogs{BundleLogs: innerRes.BodyLogs}) + } + } else { + return res, ErrInvalidBundle + } + + coinbaseDelta.Set(statedb.GetBalance(header.Coinbase)) + coinbaseDelta.Sub(coinbaseDelta, coinbaseBefore) + + res.TotalProfit.Add(res.TotalProfit, coinbaseDelta) + if !refundIdx[i] { + res.RefundableValue.Add(res.RefundableValue, coinbaseDelta) + } + } + + // estimate payout value and subtract from total profit + signer := types.MakeSigner(config, header.Number) + for i, el := range refundPercents { + if !refundIdx[i] { + continue + } + // we pay tx cost out of the refundable value + + // cost + refundConfig, err := types.GetRefundConfig(&b.Body[i], signer) + if err != nil { + return res, err + } + payoutTxFee := new(big.Int).Mul(header.BaseFee, SbundlePayoutMaxCost) + payoutTxFee.Mul(payoutTxFee, new(big.Int).SetInt64(int64(len(refundConfig)))) + res.GasUsed += SbundlePayoutMaxCost.Uint64() * uint64(len(refundConfig)) + + // allocated refundable value + payoutValue := common.PercentOf(res.RefundableValue, el) + + if payoutTxFee.Cmp(payoutValue) > 0 { + return res, ErrNegativeProfit + } + + res.TotalProfit.Sub(res.TotalProfit, payoutValue) + } + + if res.TotalProfit.Sign() < 0 { + res.TotalProfit.Set(common.Big0) + return res, ErrNegativeProfit + } + res.MevGasPrice.Div(res.TotalProfit, new(big.Int).SetUint64(res.GasUsed)) + return res, nil +} diff --git a/core/state/multi_tx_snapshot.go b/core/state/multi_tx_snapshot.go new file mode 100644 index 0000000000..f70842a98f --- /dev/null +++ b/core/state/multi_tx_snapshot.go @@ -0,0 +1,692 @@ +package state + +import ( + "errors" + "fmt" + "math/big" + "reflect" + + "github.com/ethereum/go-ethereum/common" +) + +// MultiTxSnapshot retains StateDB changes for multiple transactions. +type MultiTxSnapshot struct { + invalid bool + + numLogsAdded map[common.Hash]int + + prevObjects map[common.Address]*stateObject + + accountStorage map[common.Address]map[common.Hash]*common.Hash + accountBalance map[common.Address]*big.Int + accountNonce map[common.Address]uint64 + accountCode map[common.Address][]byte + accountCodeHash map[common.Address][]byte + + accountSuicided map[common.Address]bool + accountDeleted map[common.Address]bool + + accountNotPending map[common.Address]struct{} + accountNotDirty map[common.Address]struct{} + + // touched accounts are accounts that can be affected when snapshot is reverted + // we clear dirty storage for touched accounts when snapshot is reverted + touchedAccounts map[common.Address]struct{} + + // TODO: snapdestructs, snapaccount storage +} + +// NewMultiTxSnapshot creates a new MultiTxSnapshot +func NewMultiTxSnapshot() *MultiTxSnapshot { + multiTxSnapshot := newMultiTxSnapshot() + return &multiTxSnapshot +} + +func newMultiTxSnapshot() MultiTxSnapshot { + return MultiTxSnapshot{ + numLogsAdded: make(map[common.Hash]int), + prevObjects: make(map[common.Address]*stateObject), + accountStorage: make(map[common.Address]map[common.Hash]*common.Hash), + accountBalance: make(map[common.Address]*big.Int), + accountNonce: make(map[common.Address]uint64), + accountCode: make(map[common.Address][]byte), + accountCodeHash: make(map[common.Address][]byte), + accountSuicided: make(map[common.Address]bool), + accountDeleted: make(map[common.Address]bool), + accountNotPending: make(map[common.Address]struct{}), + accountNotDirty: make(map[common.Address]struct{}), + touchedAccounts: make(map[common.Address]struct{}), + } +} + +func (s MultiTxSnapshot) Copy() MultiTxSnapshot { + newSnapshot := newMultiTxSnapshot() + newSnapshot.invalid = s.invalid + + for txHash, numLogs := range s.numLogsAdded { + newSnapshot.numLogsAdded[txHash] = numLogs + } + + for address, object := range s.prevObjects { + newSnapshot.prevObjects[address] = object + } + + for address, storage := range s.accountStorage { + newSnapshot.accountStorage[address] = make(map[common.Hash]*common.Hash) + for key, value := range storage { + newSnapshot.accountStorage[address][key] = value + } + } + + for address, balance := range s.accountBalance { + newSnapshot.accountBalance[address] = balance + } + + for address, nonce := range s.accountNonce { + newSnapshot.accountNonce[address] = nonce + } + + for address, code := range s.accountCode { + newSnapshot.accountCode[address] = code + } + + for address, codeHash := range s.accountCodeHash { + newSnapshot.accountCodeHash[address] = codeHash + } + + for address, suicided := range s.accountSuicided { + newSnapshot.accountSuicided[address] = suicided + } + + for address, deleted := range s.accountDeleted { + newSnapshot.accountDeleted[address] = deleted + } + + for address := range s.accountNotPending { + newSnapshot.accountNotPending[address] = struct{}{} + } + + for address := range s.accountNotDirty { + newSnapshot.accountNotDirty[address] = struct{}{} + } + + for address := range s.touchedAccounts { + newSnapshot.touchedAccounts[address] = struct{}{} + } + + return newSnapshot +} + +// Equal returns true if the two MultiTxSnapshot are equal +func (s *MultiTxSnapshot) Equal(other *MultiTxSnapshot) bool { + if other == nil { + return false + } + if s.invalid != other.invalid { + return false + } + + visited := make(map[common.Address]bool) + for address, obj := range other.prevObjects { + current, exist := s.prevObjects[address] + if !exist { + return false + } + if current == nil && obj != nil { + return false + } + + if current != nil && obj == nil { + return false + } + + visited[address] = true + } + + for address, obj := range s.prevObjects { + if visited[address] { + continue + } + + otherObject, exist := other.prevObjects[address] + if !exist { + return false + } + + if otherObject == nil && obj != nil { + return false + } + + if otherObject != nil && obj == nil { + return false + } + } + + return reflect.DeepEqual(s.numLogsAdded, other.numLogsAdded) && + reflect.DeepEqual(s.accountStorage, other.accountStorage) && + reflect.DeepEqual(s.accountBalance, other.accountBalance) && + reflect.DeepEqual(s.accountNonce, other.accountNonce) && + reflect.DeepEqual(s.accountCode, other.accountCode) && + reflect.DeepEqual(s.accountCodeHash, other.accountCodeHash) && + reflect.DeepEqual(s.accountSuicided, other.accountSuicided) && + reflect.DeepEqual(s.accountDeleted, other.accountDeleted) && + reflect.DeepEqual(s.accountNotPending, other.accountNotPending) && + reflect.DeepEqual(s.accountNotDirty, other.accountNotDirty) && + reflect.DeepEqual(s.touchedAccounts, other.touchedAccounts) +} + +// updateFromJournal updates the snapshot with the changes from the journal. +func (s *MultiTxSnapshot) updateFromJournal(journal *journal) { + for _, journalEntry := range journal.entries { + switch entry := journalEntry.(type) { + case balanceChange: + s.updateBalanceChange(entry) + case nonceChange: + s.updateNonceChange(entry) + case codeChange: + s.updateCodeChange(entry) + case addLogChange: + s.numLogsAdded[entry.txhash]++ + case createObjectChange: + s.updateCreateObjectChange(entry) + case resetObjectChange: + s.updateResetObjectChange(entry) + case suicideChange: + s.updateSuicideChange(entry) + } + } +} + +// objectChanged returns whether the object was changed (in the set of prevObjects), which can happen +// because of self-destructs and deployments. +func (s *MultiTxSnapshot) objectChanged(address common.Address) bool { + _, ok := s.prevObjects[address] + return ok +} + +// updateBalanceChange updates the snapshot with the balance change. +func (s *MultiTxSnapshot) updateBalanceChange(change balanceChange) { + s.touchedAccounts[*change.account] = struct{}{} + if s.objectChanged(*change.account) { + return + } + if _, ok := s.accountBalance[*change.account]; !ok { + s.accountBalance[*change.account] = change.prev + } +} + +// updateNonceChange updates the snapshot with the nonce change. +func (s *MultiTxSnapshot) updateNonceChange(change nonceChange) { + s.touchedAccounts[*change.account] = struct{}{} + if s.objectChanged(*change.account) { + return + } + if _, ok := s.accountNonce[*change.account]; !ok { + s.accountNonce[*change.account] = change.prev + } +} + +// updateCodeChange updates the snapshot with the code change. +func (s *MultiTxSnapshot) updateCodeChange(change codeChange) { + s.touchedAccounts[*change.account] = struct{}{} + if s.objectChanged(*change.account) { + return + } + if _, ok := s.accountCode[*change.account]; !ok { + s.accountCode[*change.account] = change.prevcode + s.accountCodeHash[*change.account] = change.prevhash + } +} + +// updateResetObjectChange updates the snapshot with the reset object change. +func (s *MultiTxSnapshot) updateResetObjectChange(change resetObjectChange) { + s.touchedAccounts[change.prev.address] = struct{}{} + address := change.prev.address + if _, ok := s.prevObjects[address]; !ok { + s.prevObjects[address] = change.prev + } +} + +// updateCreateObjectChange updates the snapshot with the createObjectChange. +func (s *MultiTxSnapshot) updateCreateObjectChange(change createObjectChange) { + s.touchedAccounts[*change.account] = struct{}{} + if _, ok := s.prevObjects[*change.account]; !ok { + s.prevObjects[*change.account] = nil + } +} + +// updateSuicideChange updates the snapshot with the suicide change. +func (s *MultiTxSnapshot) updateSuicideChange(change suicideChange) { + s.touchedAccounts[*change.account] = struct{}{} + if s.objectChanged(*change.account) { + return + } + if _, ok := s.accountSuicided[*change.account]; !ok { + s.accountSuicided[*change.account] = change.prev + } + if _, ok := s.accountBalance[*change.account]; !ok { + s.accountBalance[*change.account] = change.prevbalance + } +} + +// updatePendingStorage updates the snapshot with the pending storage change. +func (s *MultiTxSnapshot) updatePendingStorage(address common.Address, key, value common.Hash, ok bool) { + s.touchedAccounts[address] = struct{}{} + if s.objectChanged(address) { + return + } + if _, exists := s.accountStorage[address]; !exists { + s.accountStorage[address] = make(map[common.Hash]*common.Hash) + } + if _, exists := s.accountStorage[address][key]; exists { + return + } + if ok { + s.accountStorage[address][key] = &value + } else { + s.accountStorage[address][key] = nil + } +} + +// updatePendingStatus updates the snapshot with previous pending status. +func (s *MultiTxSnapshot) updatePendingStatus(address common.Address, pending, dirty bool) { + s.touchedAccounts[address] = struct{}{} + if !pending { + s.accountNotPending[address] = struct{}{} + } + if !dirty { + s.accountNotDirty[address] = struct{}{} + } +} + +// updateObjectDeleted updates the snapshot with the object deletion. +func (s *MultiTxSnapshot) updateObjectDeleted(address common.Address, deleted bool) { + s.touchedAccounts[address] = struct{}{} + if s.objectChanged(address) { + return + } + if _, ok := s.accountDeleted[address]; !ok { + s.accountDeleted[address] = deleted + } +} + +// Merge merges the changes from another snapshot into the current snapshot. +// The operation assumes that the other snapshot is later (newer) than the current snapshot. +// Changes are merged such that older state is retained and not overwritten. +// In other words, this method performs a union operation on two snapshots, where +// older values are retained and any new values are added to the current snapshot. +func (s *MultiTxSnapshot) Merge(other *MultiTxSnapshot) error { + if other.invalid || s.invalid { + return errors.New("failed to merge snapshots - invalid snapshot found") + } + + // each snapshot increments the number of logs per transaction hash + // when we merge snapshots, the number of logs added per transaction are appended to current snapshot + for txHash, numLogs := range other.numLogsAdded { + s.numLogsAdded[txHash] += numLogs + } + + // prevObjects contain mapping of address to state objects + // if the current snapshot has previous object for same address, retain previous object + // otherwise, add new object from other snapshot + for address, object := range other.prevObjects { + if _, exist := s.prevObjects[address]; !exist { + s.prevObjects[address] = object + } + } + + // merge account storage - + // we want to retain any existing storage values for a given account, + // update storage keys if they do not exist for a given account's storage, + // and update pending storage for accounts that don't already exist in current snapshot + for address, storage := range other.accountStorage { + if s.objectChanged(address) { + continue + } + + if _, exist := s.accountStorage[address]; !exist { + s.accountStorage[address] = make(map[common.Hash]*common.Hash) + s.accountStorage[address] = storage + continue + } + + for key, value := range storage { + if _, exists := s.accountStorage[address][key]; !exists { + s.accountStorage[address][key] = value + } + } + } + + // add previous balance(s) for any addresses that don't exist in current snapshot + for address, balance := range other.accountBalance { + if s.objectChanged(address) { + continue + } + + if _, exist := s.accountBalance[address]; !exist { + s.accountBalance[address] = balance + } + } + + // add previous nonce for accounts that don't exist in current snapshot + for address, nonce := range other.accountNonce { + if s.objectChanged(address) { + continue + } + if _, exist := s.accountNonce[address]; !exist { + s.accountNonce[address] = nonce + } + } + + // add previous code for accounts not found in current snapshot + for address, code := range other.accountCode { + if s.objectChanged(address) { + continue + } + if _, exist := s.accountCode[address]; !exist { + if _, found := other.accountCodeHash[address]; !found { + // every codeChange has code and code hash set - + // should never reach this point unless there is programming error + panic("snapshot merge found code but no code hash for account address") + } + + s.accountCode[address] = code + s.accountCodeHash[address] = other.accountCodeHash[address] + } + } + + // add previous suicide for addresses not in current snapshot + for address, suicided := range other.accountSuicided { + if s.objectChanged(address) { + continue + } + + if _, exist := s.accountSuicided[address]; !exist { + s.accountSuicided[address] = suicided + } else { + return errors.New("failed to merge snapshots - duplicate found for account suicide") + } + } + + // add previous account deletions if they don't exist + for address, deleted := range other.accountDeleted { + if s.objectChanged(address) { + continue + } + if _, exist := s.accountDeleted[address]; !exist { + s.accountDeleted[address] = deleted + } + } + + // add previous pending status if not found + for address := range other.accountNotPending { + if _, exist := s.accountNotPending[address]; !exist { + s.accountNotPending[address] = struct{}{} + } + } + + for address := range other.accountNotDirty { + if _, exist := s.accountNotDirty[address]; !exist { + s.accountNotDirty[address] = struct{}{} + } + } + + for address := range other.touchedAccounts { + s.touchedAccounts[address] = struct{}{} + } + + return nil +} + +// revertState reverts the state to the snapshot. +func (s *MultiTxSnapshot) revertState(st *StateDB) { + // remove all the logs added + for txhash, numLogs := range s.numLogsAdded { + lens := len(st.logs[txhash]) + if lens == numLogs { + delete(st.logs, txhash) + } else { + st.logs[txhash] = st.logs[txhash][:lens-numLogs] + } + st.logSize -= uint(numLogs) + } + + // restore the objects + for address, object := range s.prevObjects { + if object == nil { + delete(st.stateObjects, address) + } else { + st.stateObjects[address] = object + } + } + + // restore storage + for address, storage := range s.accountStorage { + st.stateObjects[address].dirtyStorage = make(Storage) + for key, value := range storage { + if value == nil { + if _, ok := st.stateObjects[address].pendingStorage[key]; !ok { + panic(fmt.Sprintf("storage key %x not found in pending storage", key)) + } + delete(st.stateObjects[address].pendingStorage, key) + } else { + if _, ok := st.stateObjects[address].pendingStorage[key]; !ok { + panic(fmt.Sprintf("storage key %x not found in pending storage", key)) + } + st.stateObjects[address].pendingStorage[key] = *value + } + } + } + + // restore balance + for address, balance := range s.accountBalance { + st.stateObjects[address].setBalance(balance) + } + // restore nonce + for address, nonce := range s.accountNonce { + st.stateObjects[address].setNonce(nonce) + } + // restore code + for address, code := range s.accountCode { + st.stateObjects[address].setCode(common.BytesToHash(s.accountCodeHash[address]), code) + } + // restore suicided + for address, suicided := range s.accountSuicided { + st.stateObjects[address].suicided = suicided + } + // restore deleted + for address, deleted := range s.accountDeleted { + st.stateObjects[address].deleted = deleted + } + + // restore pending status + for address := range s.accountNotPending { + delete(st.stateObjectsPending, address) + } + for address := range s.accountNotDirty { + delete(st.stateObjectsDirty, address) + } + + // clean dirty state of touched accounts + for address := range s.touchedAccounts { + if obj, ok := st.stateObjects[address]; ok { + obj.dirtyStorage = make(Storage) + } + } +} + +// MultiTxSnapshotStack contains a list of snapshots for multiple transactions associated with a StateDB. +// Intended use is as follows: +// - Create a new snapshot and push on top of the stack +// - Apply transactions to state and update head snapshot with changes from journal +// - If any changes applied to state database are committed to trie, invalidate the head snapshot +// - If applied changes are not desired, revert the changes from the head snapshot and pop the snapshot from the stack +// - If applied changes are desired, commit the changes from the head snapshot by merging with previous entry +// and pop the snapshot from the stack +type MultiTxSnapshotStack struct { + snapshots []MultiTxSnapshot + state *StateDB +} + +// NewMultiTxSnapshotStack creates a new MultiTxSnapshotStack with a given StateDB. +func NewMultiTxSnapshotStack(state *StateDB) *MultiTxSnapshotStack { + return &MultiTxSnapshotStack{ + snapshots: make([]MultiTxSnapshot, 0), + state: state, + } +} + +// NewSnapshot creates a new snapshot and pushes it on top of the stack. +func (stack *MultiTxSnapshotStack) NewSnapshot() (*MultiTxSnapshot, error) { + if len(stack.snapshots) > 0 && stack.snapshots[len(stack.snapshots)-1].invalid { + return nil, errors.New("failed to create new multi-transaction snapshot - invalid snapshot found at head") + } + + snap := newMultiTxSnapshot() + stack.snapshots = append(stack.snapshots, snap) + return &snap, nil +} + +func (stack *MultiTxSnapshotStack) Copy(statedb *StateDB) *MultiTxSnapshotStack { + newStack := NewMultiTxSnapshotStack(statedb) + for _, snapshot := range stack.snapshots { + newStack.snapshots = append(newStack.snapshots, snapshot.Copy()) + } + return newStack +} + +// Peek returns the snapshot at the top of the stack. +func (stack *MultiTxSnapshotStack) Peek() *MultiTxSnapshot { + if len(stack.snapshots) == 0 { + return nil + } + return &stack.snapshots[len(stack.snapshots)-1] +} + +// Pop removes the snapshot at the top of the stack and returns it. +func (stack *MultiTxSnapshotStack) Pop() (*MultiTxSnapshot, error) { + size := len(stack.snapshots) + if size == 0 { + return nil, errors.New("failed to revert multi-transaction snapshot - does not exist") + } + + head := &stack.snapshots[size-1] + stack.snapshots = stack.snapshots[:size-1] + return head, nil +} + +// Revert rewinds the changes from the head snapshot and removes it from the stack. +func (stack *MultiTxSnapshotStack) Revert() (*MultiTxSnapshot, error) { + size := len(stack.snapshots) + if size == 0 { + return nil, errors.New("failed to revert multi-transaction snapshot - does not exist") + } + + head := &stack.snapshots[size-1] + if head.invalid { + return nil, errors.New("failed to revert multi-transaction snapshot - invalid snapshot found") + } + + head.revertState(stack.state) + stack.snapshots = stack.snapshots[:size-1] + return head, nil +} + +// RevertAll reverts all snapshots in the stack. +func (stack *MultiTxSnapshotStack) RevertAll() (snapshot *MultiTxSnapshot, err error) { + for len(stack.snapshots) > 0 { + if snapshot, err = stack.Revert(); err != nil { + break + } + } + return +} + +// Commit merges the changes from the head snapshot with the previous snapshot and removes it from the stack. +func (stack *MultiTxSnapshotStack) Commit() (*MultiTxSnapshot, error) { + if len(stack.snapshots) == 0 { + return nil, errors.New("failed to commit multi-transaction snapshot - does not exist") + } + + if len(stack.snapshots) == 1 { + return stack.Pop() + } + + var ( + head *MultiTxSnapshot + err error + ) + if head, err = stack.Pop(); err != nil { + return nil, err + } + + current := stack.Peek() + if err = current.Merge(head); err != nil { + return nil, err + } + + stack.snapshots[len(stack.snapshots)-1] = *current + return head, nil +} + +// Size returns the number of snapshots in the stack. +func (stack *MultiTxSnapshotStack) Size() int { + return len(stack.snapshots) +} + +// Invalidate invalidates the latest snapshot. This is used when state changes are committed to trie. +func (stack *MultiTxSnapshotStack) Invalidate() { + size := len(stack.snapshots) + if size == 0 { + return + } + + head := stack.snapshots[size-1] + head.invalid = true + stack.snapshots = stack.snapshots[:0] + stack.snapshots = append(stack.snapshots, head) +} + +// UpdatePendingStatus updates the pending status for an address. +func (stack *MultiTxSnapshotStack) UpdatePendingStatus(address common.Address, pending, dirty bool) { + if len(stack.snapshots) == 0 { + return + } + + current := stack.Peek() + current.updatePendingStatus(address, pending, dirty) + stack.snapshots[len(stack.snapshots)-1] = *current +} + +// UpdatePendingStorage updates the pending storage for an address. +func (stack *MultiTxSnapshotStack) UpdatePendingStorage(address common.Address, key, value common.Hash, ok bool) { + if len(stack.snapshots) == 0 { + return + } + + current := stack.Peek() + current.updatePendingStorage(address, key, value, ok) + stack.snapshots[len(stack.snapshots)-1] = *current +} + +// UpdateFromJournal updates the snapshot with the changes from the journal. +func (stack *MultiTxSnapshotStack) UpdateFromJournal(journal *journal) { + if len(stack.snapshots) == 0 { + return + } + + current := stack.Peek() + current.updateFromJournal(journal) + stack.snapshots[len(stack.snapshots)-1] = *current +} + +// UpdateObjectDeleted updates the snapshot with the object deletion. +func (stack *MultiTxSnapshotStack) UpdateObjectDeleted(address common.Address, deleted bool) { + if len(stack.snapshots) == 0 { + return + } + + current := stack.Peek() + current.updateObjectDeleted(address, deleted) + stack.snapshots[len(stack.snapshots)-1] = *current +} diff --git a/core/state/statedb.go b/core/state/statedb.go index 9e680fae61..857ef1d58b 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -118,6 +118,9 @@ type StateDB struct { validRevisions []revision nextRevisionId int + // Multi-Transaction Snapshot Stack + multiTxSnapshotStack *MultiTxSnapshotStack + // Measurements gathered during execution for debugging purposes AccountReads time.Duration AccountHashes time.Duration @@ -162,6 +165,8 @@ func New(root common.Hash, db Database, snaps *snapshot.Tree) (*StateDB, error) transientStorage: newTransientStorage(), hasher: crypto.NewKeccakState(), } + + sdb.multiTxSnapshotStack = NewMultiTxSnapshotStack(sdb) if sdb.snaps != nil { if sdb.snap = sdb.snaps.Snapshot(root); sdb.snap != nil { sdb.snapAccounts = make(map[common.Hash][]byte) @@ -1141,6 +1146,8 @@ func (s *StateDB) Copy() *StateDB { journal: newJournal(), hasher: crypto.NewKeccakState(), } + // Initialize copy of multi-transaction snapshot stack for the copied state + state.multiTxSnapshotStack = s.multiTxSnapshotStack.Copy(state) // Copy the dirty states, logs, and preimages for addr := range s.journal.dirties { // As documented [here](https://github.com/ethereum/go-ethereum/pull/16485#issuecomment-380438527), @@ -1278,6 +1285,8 @@ func (s *StateDB) GetRefund() uint64 { // the journal as well as the refunds. Finalise, however, will not push any updates // into the tries just yet. Only IntermediateRoot or Commit will do that. func (s *StateDB) Finalise(deleteEmptyObjects bool) { + s.multiTxSnapshotStack.UpdateFromJournal(s.journal) + addressesToPrefetch := make([][]byte, 0, len(s.journal.dirties)) for addr := range s.journal.dirties { @@ -1293,6 +1302,8 @@ func (s *StateDB) Finalise(deleteEmptyObjects bool) { } if obj.suicided || (deleteEmptyObjects && obj.empty()) { + s.multiTxSnapshotStack.UpdateObjectDeleted(obj.address, obj.deleted) + obj.deleted = true // We need to maintain account deletions explicitly (will remain @@ -1311,6 +1322,11 @@ func (s *StateDB) Finalise(deleteEmptyObjects bool) { obj.finalise(true) // Prefetch slots in the background } + if s.multiTxSnapshotStack.Size() > 0 { + _, wasPending := s.stateObjectsPending[addr] + _, wasDirty := s.stateObjectsDirty[addr] + s.multiTxSnapshotStack.UpdatePendingStatus(addr, wasPending, wasDirty) + } s.stateObjectsPending[addr] = struct{}{} s.stateObjectsDirty[addr] = struct{}{} @@ -1334,6 +1350,10 @@ func (s *StateDB) IntermediateRoot(deleteEmptyObjects bool) common.Hash { // Finalise all the dirty storage states and write them into the tries s.Finalise(deleteEmptyObjects) + // Intermediate root writes updates to the trie, which will cause + // in memory multi-transaction snapshot to be incompatible with the committed state, so we invalidate. + s.multiTxSnapshotStack.Invalidate() + // If there was a trie prefetcher operating, it gets aborted and irrevocably // modified after we start retrieving tries. Remove it from the statedb after // this round of use. @@ -1688,3 +1708,22 @@ func (s *StateDB) convertAccountSet(set map[common.Address]struct{}) map[common. return ret } + +func (s *StateDB) NewMultiTxSnapshot() (err error) { + _, err = s.multiTxSnapshotStack.NewSnapshot() + return +} + +func (s *StateDB) MultiTxSnapshotRevert() (err error) { + _, err = s.multiTxSnapshotStack.Revert() + return +} + +func (s *StateDB) MultiTxSnapshotCommit() (err error) { + _, err = s.multiTxSnapshotStack.Commit() + return +} + +func (s *StateDB) MultiTxSnapshotStackSize() int { + return s.multiTxSnapshotStack.Size() +} diff --git a/core/txpool/sbundle_pool.go b/core/txpool/sbundle_pool.go new file mode 100644 index 0000000000..e4a0e53952 --- /dev/null +++ b/core/txpool/sbundle_pool.go @@ -0,0 +1,250 @@ +package txpool + +// TODO: cancel sbundles, fetch them from the db + +import ( + "errors" + "fmt" + "sync" + "sync/atomic" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/params" +) + +const ( + maxSBundleRange = 30 + maxSBundleNesting = 1 +) + +var ( + ErrInvalidInclusion = errors.New("invalid inclusion") + ErrBundleTooDeep = errors.New("bundle too deep") + ErrInvalidBody = errors.New("invalid body") + ErrInvalidConstraints = errors.New("invalid constraints") +) + +type SBundlePool struct { + mu sync.Mutex + + bundles map[common.Hash]*types.SBundle + byBlock map[uint64][]*types.SBundle + + // bundles that were cancelled and their max valid block + cancelled map[common.Hash]struct{} + cancelledMaxBlock map[uint64][]common.Hash + + signer types.Signer + + // data from tx_pool that is constantly updated + istanbul atomic.Bool + eip2718 atomic.Bool + eip1559 atomic.Bool + shanghai atomic.Bool + currentMaxGas atomic.Uint64 +} + +func NewSBundlePool(signer types.Signer) *SBundlePool { + return &SBundlePool{ + bundles: make(map[common.Hash]*types.SBundle), + byBlock: make(map[uint64][]*types.SBundle), + cancelled: make(map[common.Hash]struct{}), + cancelledMaxBlock: make(map[uint64][]common.Hash), + signer: signer, + } +} + +func (p *SBundlePool) ResetPoolData(pool *TxPool) { + p.mu.Lock() + defer p.mu.Unlock() + + p.istanbul = pool.istanbul + p.eip2718 = pool.eip2718 + p.eip1559 = pool.eip1559 + p.shanghai = pool.shanghai + p.currentMaxGas = pool.currentMaxGas +} + +func (p *SBundlePool) Add(bundle *types.SBundle) error { + p.mu.Lock() + defer p.mu.Unlock() + + if _, ok := p.bundles[bundle.Hash()]; ok { + return nil + } + + if err := p.validateSBundle(0, bundle); err != nil { + return err + } + + p.bundles[bundle.Hash()] = bundle + for b := bundle.Inclusion.BlockNumber; b <= bundle.Inclusion.MaxBlockNumber; b++ { + p.byBlock[b] = append(p.byBlock[b], bundle) + } + return nil +} + +func (p *SBundlePool) GetSBundles(nextBlock uint64) []*types.SBundle { + p.mu.Lock() + defer p.mu.Unlock() + + // remove old blocks + for b, el := range p.byBlock { + if b < nextBlock { + for _, bundle := range el { + if bundle.Inclusion.MaxBlockNumber < nextBlock { + delete(p.bundles, bundle.Hash()) + } + delete(p.bundles, bundle.Hash()) + } + delete(p.byBlock, b) + } + } + + // remove expired cancelled bundles + for b, el := range p.cancelledMaxBlock { + if b < nextBlock { + for _, hash := range el { + delete(p.cancelled, hash) + } + delete(p.cancelledMaxBlock, b) + } + } + + // filter cancelled bundles and dependent bundles + var res []*types.SBundle + for _, bundle := range p.byBlock[nextBlock] { + if isBundleCancelled(bundle, p.cancelled) { + continue + } + res = append(res, bundle) + } + + return res +} + +func (p *SBundlePool) validateSBundle(level int, b *types.SBundle) error { + if level > maxSBundleNesting { + return ErrBundleTooDeep + } + // inclusion + if b.Inclusion.BlockNumber > b.Inclusion.MaxBlockNumber { + return ErrInvalidInclusion + } + if b.Inclusion.MaxBlockNumber-b.Inclusion.BlockNumber > maxSBundleRange { + return ErrInvalidInclusion + } + + // body + for _, el := range b.Body { + if el.Tx != nil { + if err := p.validateTx(el.Tx); err != nil { + return err + } + } else if el.Bundle != nil { + if err := p.validateSBundle(level+1, el.Bundle); err != nil { + return err + } + } else { + return ErrInvalidBody + } + } + + // constraints + if len(b.Validity.Refund) > len(b.Body) { + return ErrInvalidConstraints + } + + usedConstraints := make([]bool, len(b.Body)) + totalRefundPercent := 0 + for _, el := range b.Validity.Refund { + if el.BodyIdx >= len(b.Body) { + return ErrInvalidConstraints + } + if usedConstraints[el.BodyIdx] { + return ErrInvalidConstraints + } + usedConstraints[el.BodyIdx] = true + totalRefundPercent += el.Percent + } + if totalRefundPercent > 100 { + return ErrInvalidConstraints + } + + return nil +} + +// same as core/tx_pool.go but we don't check for gas price and nonce +func (p *SBundlePool) validateTx(tx *types.Transaction) error { + // Accept only legacy transactions until EIP-2718/2930 activates. + if !p.eip2718.Load() && tx.Type() != types.LegacyTxType { + return core.ErrTxTypeNotSupported + } + // Reject dynamic fee transactions until EIP-1559 activates. + if !p.eip1559.Load() && tx.Type() == types.DynamicFeeTxType { + return core.ErrTxTypeNotSupported + } + // Reject transactions over defined size to prevent DOS attacks + if tx.Size() > txMaxSize { + return ErrOversizedData + } + // Check whether the init code size has been exceeded. + if p.shanghai.Load() && tx.To() == nil && len(tx.Data()) > params.MaxInitCodeSize { + return fmt.Errorf("%w: code size %v limit %v", core.ErrMaxInitCodeSizeExceeded, len(tx.Data()), params.MaxInitCodeSize) + } + // Transactions can't be negative. This may never happen using RLP decoded + // transactions but may occur if you create a transaction using the RPC. + if tx.Value().Sign() < 0 { + return ErrNegativeValue + } + // Ensure the transaction doesn't exceed the current block limit gas. + if p.currentMaxGas.Load() < tx.Gas() { + return ErrGasLimit + } + // Sanity check for extremely large numbers + if tx.GasFeeCap().BitLen() > 256 { + return core.ErrFeeCapVeryHigh + } + if tx.GasTipCap().BitLen() > 256 { + return core.ErrTipVeryHigh + } + // Ensure gasFeeCap is greater than or equal to gasTipCap. + if tx.GasFeeCapIntCmp(tx.GasTipCap()) < 0 { + return core.ErrTipAboveFeeCap + } + // Make sure the transaction is signed properly. + _, err := types.Sender(p.signer, tx) + if err != nil { + return ErrInvalidSender + } + return nil +} + +func (b *SBundlePool) Cancel(hashes []common.Hash) { + b.mu.Lock() + defer b.mu.Unlock() + + for _, hash := range hashes { + if bundle, ok := b.bundles[hash]; ok { + maxBlock := bundle.Inclusion.MaxBlockNumber + b.cancelled[hash] = struct{}{} + b.cancelledMaxBlock[maxBlock] = append(b.cancelledMaxBlock[maxBlock], hash) + } + } +} + +func isBundleCancelled(bundle *types.SBundle, cancelled map[common.Hash]struct{}) bool { + if _, ok := cancelled[bundle.Hash()]; ok { + return true + } + for _, el := range bundle.Body { + if el.Bundle != nil { + if isBundleCancelled(el.Bundle, cancelled) { + return true + } + } + } + return false +} diff --git a/core/txpool/txpool.go b/core/txpool/txpool.go index 82ee2fafa8..2a60bd39ef 100644 --- a/core/txpool/txpool.go +++ b/core/txpool/txpool.go @@ -21,6 +21,7 @@ import ( "context" "errors" "fmt" + "github.com/google/uuid" "math" "math/big" "sort" @@ -42,6 +43,7 @@ import ( "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/metrics" "github.com/ethereum/go-ethereum/params" + "golang.org/x/crypto/sha3" ) const ( @@ -104,8 +106,9 @@ var ( ) var ( - evictionInterval = time.Minute // Time interval to check for evictable transactions - statsReportInterval = 8 * time.Second // Time interval to report transaction pool stats + evictionInterval = time.Minute // Time interval to check for evictable transactions + statsReportInterval = 8 * time.Second // Time interval to report transaction pool stats + privateTxCleanupInterval = 1 * time.Hour ) var ( @@ -188,6 +191,9 @@ type Config struct { Lifetime time.Duration // Maximum amount of time non-executable transaction are queued AllowUnprotectedTxs bool // Allow non-EIP-155 transactions + PrivateTxLifetime time.Duration // Maximum amount of time to keep private transactions private + + TrustedRelays []common.Address // Trusted relay addresses. Duplicated from the miner config. } // DefaultConfig contains the default configurations for the transaction @@ -206,6 +212,7 @@ var DefaultConfig = Config{ Lifetime: 3 * time.Hour, AllowUnprotectedTxs: false, + PrivateTxLifetime: 3 * 24 * time.Hour, } // sanitize checks the provided user configurations and changes anything that's @@ -251,7 +258,10 @@ func (config *Config) sanitize() Config { log.Warn("Sanitizing invalid txpool lifetime", "provided", conf.Lifetime, "updated", DefaultConfig.Lifetime) conf.Lifetime = DefaultConfig.Lifetime } - + if conf.PrivateTxLifetime < 1 { + log.Warn("Sanitizing invalid txpool private tx lifetime", "provided", conf.PrivateTxLifetime, "updated", DefaultConfig.PrivateTxLifetime) + conf.PrivateTxLifetime = DefaultConfig.PrivateTxLifetime + } return conf } @@ -308,6 +318,11 @@ type TxPool struct { changesSinceReorg int // A counter for how many drops we've performed in-between reorg. promoteTxCh chan struct{} // should be used only for tests + + privateTxs *timestampedTxHashSet + mevBundles []types.MevBundle + bundleFetcher IFetcher + sbundles *SBundlePool } type txpoolResetRequest struct { @@ -339,6 +354,8 @@ func NewTxPool(config Config, chainconfig *params.ChainConfig, chain blockChain, initDoneCh: make(chan struct{}), gasPrice: new(big.Int).SetUint64(config.PriceLimit), gasPriceUint: uint256.NewInt(config.PriceLimit), + privateTxs: newExpiringTxHashSet(config.PrivateTxLifetime), + sbundles: NewSBundlePool(types.LatestSigner(chainconfig)), } pool.locals = newAccountSet(pool.signer) @@ -382,6 +399,17 @@ func NewTxPool(config Config, chainconfig *params.ChainConfig, chain blockChain, return pool } +type IFetcher interface { + GetLatestUuidBundles(ctx context.Context, blockNum int64) ([]types.LatestUuidBundle, error) +} + +func (pool *TxPool) RegisterBundleFetcher(fetcher IFetcher) { + pool.mu.Lock() + defer pool.mu.Unlock() + + pool.bundleFetcher = fetcher +} + // loop is the transaction pool's main event loop, waiting for and reacting to // outside blockchain events as well as for various reporting and transaction // eviction events. @@ -391,9 +419,10 @@ func (pool *TxPool) loop() { var ( prevPending, prevQueued, prevStales int // Start the stats reporting and transaction eviction tickers - report = time.NewTicker(statsReportInterval) - evict = time.NewTicker(evictionInterval) - journal = time.NewTicker(pool.config.Rejournal) + report = time.NewTicker(statsReportInterval) + evict = time.NewTicker(evictionInterval) + journal = time.NewTicker(pool.config.Rejournal) + privateTx = time.NewTicker(privateTxCleanupInterval) // Track the previous head headers for transaction reorgs head = pool.chain.CurrentBlock() ) @@ -401,6 +430,7 @@ func (pool *TxPool) loop() { defer report.Stop() defer evict.Stop() defer journal.Stop() + defer privateTx.Stop() // Notify tests that the init phase is done close(pool.initDoneCh) @@ -480,6 +510,10 @@ func (pool *TxPool) loop() { } pool.mu.Unlock() } + + // Remove stale hashes that must be kept private + case <-privateTx.C: + pool.privateTxs.prune() } } } @@ -638,6 +672,11 @@ func (pool *TxPool) ContentFrom(addr common.Address) (types.Transactions, types. return pending, queued } +// IsPrivateTxHash indicates whether the transaction should be shared with peers +func (pool *TxPool) IsPrivateTxHash(hash common.Hash) bool { + return pool.privateTxs.Contains(hash) +} + // Pending retrieves all currently processable transactions, grouped by origin // account and sorted by nonce. The returned transaction set is a copy and can be // freely modified by calling code. @@ -711,6 +750,177 @@ func (pool *TxPool) Pending(ctx context.Context, enforceTips bool) map[common.Ad return pending } +type uuidBundleKey struct { + Uuid uuid.UUID + SigningAddress common.Address +} + +func (pool *TxPool) fetchLatestCancellableBundles(ctx context.Context, blockNumber *big.Int) (chan []types.LatestUuidBundle, chan error) { + if pool.bundleFetcher == nil { + return nil, nil + } + errCh := make(chan error, 1) + lubCh := make(chan []types.LatestUuidBundle, 1) + go func(blockNum int64) { + lub, err := pool.bundleFetcher.GetLatestUuidBundles(ctx, blockNum) + errCh <- err + lubCh <- lub + }(blockNumber.Int64()) + return lubCh, errCh +} + +func resolveCancellableBundles(lubCh chan []types.LatestUuidBundle, errCh chan error, uuidBundles map[uuidBundleKey][]types.MevBundle) []types.MevBundle { + if lubCh == nil || errCh == nil { + return nil + } + + if len(uuidBundles) == 0 { + return nil + } + + err := <-errCh + if err != nil { + log.Error("could not fetch latest bundles uuid map", "err", err) + return nil + } + + currentCancellableBundles := []types.MevBundle{} + + log.Trace("Processing uuid bundles", "uuidBundles", uuidBundles) + + lubs := <-lubCh +LubLoop: + for _, lub := range lubs { + ubk := uuidBundleKey{lub.Uuid, lub.SigningAddress} + bundles, found := uuidBundles[ubk] + if !found { + log.Trace("missing uuid bundle", "ubk", ubk) + continue + } + + // If lub has bundle_uuid set, and we can find corresponding bundle we prefer it, if not we fallback to bundle_hash equivalence + if lub.BundleUUID != types.EmptyUUID { + for _, bundle := range bundles { + if bundle.ComputeUUID() == lub.BundleUUID { + log.Trace("adding uuid bundle", "bundle hash", bundle.Hash.String(), "lub", lub) + currentCancellableBundles = append(currentCancellableBundles, bundle) + continue LubLoop + } + } + } + + for _, bundle := range bundles { + if bundle.Hash == lub.BundleHash { + log.Trace("adding uuid bundle", "bundle hash", bundle.Hash.String(), "lub", lub) + currentCancellableBundles = append(currentCancellableBundles, bundle) + break + } + } + } + return currentCancellableBundles +} + +// MevBundles returns a list of bundles valid for the given blockNumber/blockTimestamp +// also prunes bundles that are outdated +// Returns regular bundles and a function resolving to current cancellable bundles +func (pool *TxPool) MevBundles(blockNumber *big.Int, blockTimestamp uint64) ([]types.MevBundle, chan []types.MevBundle) { + pool.mu.Lock() + defer pool.mu.Unlock() + + ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) + lubCh, errCh := pool.fetchLatestCancellableBundles(ctx, blockNumber) + + // returned values + var ret []types.MevBundle + // rolled over values + var bundles []types.MevBundle + // (uuid, signingAddress) -> list of bundles + var uuidBundles = make(map[uuidBundleKey][]types.MevBundle) + + for _, bundle := range pool.mevBundles { + // Prune outdated bundles + if (bundle.MaxTimestamp != 0 && blockTimestamp > bundle.MaxTimestamp) || blockNumber.Cmp(bundle.BlockNumber) > 0 { + continue + } + + // Roll over future bundles + if (bundle.MinTimestamp != 0 && blockTimestamp < bundle.MinTimestamp) || blockNumber.Cmp(bundle.BlockNumber) < 0 { + bundles = append(bundles, bundle) + continue + } + + // keep the bundles around internally until they need to be pruned + bundles = append(bundles, bundle) + + // TODO: omit duplicates + + // do not append to the return quite yet, check the DB for the latest bundle for that uuid + if bundle.Uuid != types.EmptyUUID { + ubk := uuidBundleKey{bundle.Uuid, bundle.SigningAddress} + uuidBundles[ubk] = append(uuidBundles[ubk], bundle) + continue + } + + // return the ones which are in time + ret = append(ret, bundle) + } + + pool.mevBundles = bundles + + cancellableBundlesCh := make(chan []types.MevBundle, 1) + go func() { + cancellableBundlesCh <- resolveCancellableBundles(lubCh, errCh, uuidBundles) + cancel() + }() + + return ret, cancellableBundlesCh +} + +// AddMevBundles adds a mev bundles to the pool +func (pool *TxPool) AddMevBundles(mevBundles []types.MevBundle) error { + pool.mu.Lock() + defer pool.mu.Unlock() + + pool.mevBundles = append(pool.mevBundles, mevBundles...) + return nil +} + +// AddMevBundle adds a mev bundle to the pool +func (pool *TxPool) AddMevBundle(txs types.Transactions, blockNumber *big.Int, replacementUuid uuid.UUID, signingAddress common.Address, minTimestamp, maxTimestamp uint64, revertingTxHashes []common.Hash) error { + bundleHasher := sha3.NewLegacyKeccak256() + for _, tx := range txs { + bundleHasher.Write(tx.Hash().Bytes()) + } + bundleHash := common.BytesToHash(bundleHasher.Sum(nil)) + + pool.mu.Lock() + defer pool.mu.Unlock() + + pool.mevBundles = append(pool.mevBundles, types.MevBundle{ + Txs: txs, + BlockNumber: blockNumber, + Uuid: replacementUuid, + SigningAddress: signingAddress, + MinTimestamp: minTimestamp, + MaxTimestamp: maxTimestamp, + RevertingTxHashes: revertingTxHashes, + Hash: bundleHash, + }) + return nil +} + +func (pool *TxPool) AddSBundle(bundle *types.SBundle) error { + return pool.sbundles.Add(bundle) +} + +func (pool *TxPool) CancelSBundles(hashes []common.Hash) { + pool.sbundles.Cancel(hashes) +} + +func (pool *TxPool) GetSBundles(block *big.Int) []*types.SBundle { + return pool.sbundles.GetSBundles(block.Uint64()) +} + // Locals retrieves the accounts currently considered local by the pool. func (pool *TxPool) Locals() []common.Address { return pool.locals.flatten() @@ -1176,13 +1386,14 @@ func (pool *TxPool) promoteTx(addr common.Address, hash common.Hash, tx *types.T // This method is used to add transactions from the RPC API and performs synchronous pool // reorganization and event propagation. func (pool *TxPool) AddLocals(txs []*types.Transaction) []error { - return pool.addTxs(txs, !pool.config.NoLocals, true) + return pool.addTxs(txs, !pool.config.NoLocals, true, false) } // AddLocal enqueues a single local transaction into the pool if it is valid. This is // a convenience wrapper around AddLocals. func (pool *TxPool) AddLocal(tx *types.Transaction) error { - return pool.addTx(tx, !pool.config.NoLocals, true) + errs := pool.AddLocals([]*types.Transaction{tx}) + return errs[0] } // AddRemotes enqueues a batch of transactions into the pool if they are valid. If the @@ -1191,16 +1402,22 @@ func (pool *TxPool) AddLocal(tx *types.Transaction) error { // This method is used to add transactions from the p2p network and does not wait for pool // reorganization and internal event propagation. func (pool *TxPool) AddRemotes(txs []*types.Transaction) []error { - return pool.addTxs(txs, false, false) + return pool.addTxs(txs, false, false, false) +} + +// AddPrivateRemote adds transactions to the pool, but does not broadcast these transactions to any peers. +func (pool *TxPool) AddPrivateRemote(tx *types.Transaction) error { + errs := pool.addTxs([]*types.Transaction{tx}, false, false, true) + return errs[0] } // AddRemotesSync is like AddRemotes, but waits for pool reorganization. Tests use this method. func (pool *TxPool) AddRemotesSync(txs []*types.Transaction) []error { - return pool.addTxs(txs, false, true) + return pool.addTxs(txs, false, true, false) } -func (pool *TxPool) AddRemoteSync(txs *types.Transaction) error { - return pool.addTx(txs, false, true) +func (pool *TxPool) AddRemoteSync(tx *types.Transaction) error { + return pool.addRemoteSync(tx) } // This is like AddRemotes with a single transaction, but waits for pool reorganization. Tests use this method. @@ -1217,7 +1434,7 @@ func (pool *TxPool) AddRemote(tx *types.Transaction) error { } // addTxs attempts to queue a batch of transactions if they are valid. -func (pool *TxPool) addTxs(txs []*types.Transaction, local, sync bool) []error { +func (pool *TxPool) addTxs(txs []*types.Transaction, local, sync bool, private bool) []error { // Filter out known ones without obtaining the pool lock or recovering signatures var ( errs = make([]error, len(txs)) @@ -1262,6 +1479,13 @@ func (pool *TxPool) addTxs(txs []*types.Transaction, local, sync bool) []error { return errs } + // Track private transactions, so they don't get leaked to the public mempool + if private { + for _, tx := range news { + pool.privateTxs.Add(tx.Hash()) + } + } + // Process all the new transaction and merge any errors into the original slice pool.mu.Lock() newErrs, dirtyAddrs := pool.addTxsLocked(news, local) @@ -1287,59 +1511,59 @@ func (pool *TxPool) addTxs(txs []*types.Transaction, local, sync bool) []error { } // addTxs attempts to queue a batch of transactions if they are valid. -func (pool *TxPool) addTx(tx *types.Transaction, local, sync bool) error { - // Filter out known ones without obtaining the pool lock or recovering signatures - var ( - err error - hash common.Hash - ) - - func() { - // If the transaction is known, pre-set the error slot - hash = tx.Hash() - - if pool.all.Get(hash) != nil { - err = ErrAlreadyKnown - - knownTxMeter.Mark(1) - - return - } - - // Exclude transactions with invalid signatures as soon as - // possible and cache senders in transactions before - // obtaining lock - if pool.config.AllowUnprotectedTxs { - pool.signer = types.NewFakeSigner(tx.ChainId()) - } - - _, err = types.Sender(pool.signer, tx) - if err != nil { - invalidTxMeter.Mark(1) - - return - } - }() - - if err != nil { - return err - } - - var dirtyAddrs *accountSet - - // Process all the new transaction and merge any errors into the original slice - pool.mu.Lock() - err, dirtyAddrs = pool.addTxLocked(tx, local) - pool.mu.Unlock() - - // Reorg the pool internals if needed and return - done := pool.requestPromoteExecutables(dirtyAddrs) - if sync { - <-done - } - - return err -} +//func (pool *TxPool) addTx(tx *types.Transaction, local, sync bool) error { +// // Filter out known ones without obtaining the pool lock or recovering signatures +// var ( +// err error +// hash common.Hash +// ) +// +// func() { +// // If the transaction is known, pre-set the error slot +// hash = tx.Hash() +// +// if pool.all.Get(hash) != nil { +// err = ErrAlreadyKnown +// +// knownTxMeter.Mark(1) +// +// return +// } +// +// // Exclude transactions with invalid signatures as soon as +// // possible and cache senders in transactions before +// // obtaining lock +// if pool.config.AllowUnprotectedTxs { +// pool.signer = types.NewFakeSigner(tx.ChainId()) +// } +// +// _, err = types.Sender(pool.signer, tx) +// if err != nil { +// invalidTxMeter.Mark(1) +// +// return +// } +// }() +// +// if err != nil { +// return err +// } +// +// var dirtyAddrs *accountSet +// +// // Process all the new transaction and merge any errors into the original slice +// pool.mu.Lock() +// err, dirtyAddrs = pool.addTxLocked(tx, local) +// pool.mu.Unlock() +// +// // Reorg the pool internals if needed and return +// done := pool.requestPromoteExecutables(dirtyAddrs) +// if sync { +// <-done +// } +// +// return err +//} // addTxsLocked attempts to queue a batch of transactions if they are valid. // The transaction pool lock must be held. @@ -1790,9 +2014,12 @@ func (pool *TxPool) runReorg(ctx context.Context, done chan struct{}, reset *txp var txs []*types.Transaction for _, set := range events { - txs = append(txs, set.Flatten()...) + for _, tx := range set.Flatten() { + if !pool.IsPrivateTxHash(tx.Hash()) { + txs = append(txs, tx) + } + } } - pool.txFeed.Send(core.NewTxsEvent{Txs: txs}) }) } @@ -1901,6 +2128,7 @@ func (pool *TxPool) reset(oldHead, newHead *types.Header) { pool.eip2718.Store(pool.chainconfig.IsBerlin(next)) pool.eip1559.Store(pool.chainconfig.IsLondon(next)) pool.shanghai.Store(pool.chainconfig.IsShanghai(uint64(time.Now().Unix()))) + pool.sbundles.ResetPoolData(pool) } // promoteExecutables moves transactions that have become processable from the @@ -2278,6 +2506,7 @@ func (pool *TxPool) demoteUnexecutables() { for _, tx := range olds { hash = tx.Hash() pool.all.Remove(hash) + pool.privateTxs.Remove(hash) log.Trace("Removed old pending transaction", "hash", hash) } @@ -2647,6 +2876,60 @@ func (t *lookup) RemotesBelowTip(threshold *big.Int) types.Transactions { return found } +type timestampedTxHashSet struct { + lock sync.RWMutex + timestamps map[common.Hash]time.Time + ttl time.Duration +} + +func newExpiringTxHashSet(ttl time.Duration) *timestampedTxHashSet { + s := ×tampedTxHashSet{ + timestamps: make(map[common.Hash]time.Time), + ttl: ttl, + } + + return s +} + +func (s *timestampedTxHashSet) Add(hash common.Hash) { + s.lock.Lock() + defer s.lock.Unlock() + + _, ok := s.timestamps[hash] + if !ok { + s.timestamps[hash] = time.Now().Add(s.ttl) + } +} + +func (s *timestampedTxHashSet) Contains(hash common.Hash) bool { + s.lock.RLock() + defer s.lock.RUnlock() + _, ok := s.timestamps[hash] + return ok +} + +func (s *timestampedTxHashSet) Remove(hash common.Hash) { + s.lock.Lock() + defer s.lock.Unlock() + + _, ok := s.timestamps[hash] + if ok { + delete(s.timestamps, hash) + } +} + +func (s *timestampedTxHashSet) prune() { + s.lock.Lock() + defer s.lock.Unlock() + + now := time.Now() + for hash, ts := range s.timestamps { + if ts.Before(now) { + delete(s.timestamps, hash) + } + } +} + // numSlots calculates the number of slots needed for a single transaction. func numSlots(tx *types.Transaction) int { return int((tx.Size() + txSlotSize - 1) / txSlotSize) diff --git a/core/types/sbundle.go b/core/types/sbundle.go new file mode 100644 index 0000000000..961922b7e7 --- /dev/null +++ b/core/types/sbundle.go @@ -0,0 +1,116 @@ +package types + +import ( + "errors" + "math/big" + "sync/atomic" + + "github.com/ethereum/go-ethereum/common" + "golang.org/x/crypto/sha3" +) + +var ( + ErrIncorrectRefundConfig = errors.New("incorrect refund config") +) + +// SBundle is a bundle of transactions that must be executed atomically +// unlike ordinary bundle it also supports refunds +type SBundle struct { + Inclusion BundleInclusion + Body []BundleBody + Validity BundleValidity + + hash atomic.Value +} + +type BundleInclusion struct { + BlockNumber uint64 + MaxBlockNumber uint64 +} + +type BundleBody struct { + Tx *Transaction + Bundle *SBundle + CanRevert bool +} + +type BundleValidity struct { + Refund []RefundConstraint `json:"refund,omitempty"` + RefundConfig []RefundConfig `json:"refundConfig,omitempty"` +} + +type RefundConstraint struct { + BodyIdx int `json:"bodyIdx"` + Percent int `json:"percent"` +} + +type RefundConfig struct { + Address common.Address `json:"address"` + Percent int `json:"percent"` +} + +type BundlePrivacy struct { + RefundAddress common.Address +} + +func (b *SBundle) Hash() common.Hash { + if hash := b.hash.Load(); hash != nil { + return hash.(common.Hash) + } + + bodyHashes := make([]common.Hash, len(b.Body)) + for i, body := range b.Body { + if body.Tx != nil { + bodyHashes[i] = body.Tx.Hash() + } else if body.Bundle != nil { + bodyHashes[i] = body.Bundle.Hash() + } + } + + var h common.Hash + if len(bodyHashes) == 1 { + h = bodyHashes[0] + } else { + hasher := sha3.NewLegacyKeccak256() + for _, h := range bodyHashes { + hasher.Write(h[:]) + } + h = common.BytesToHash(hasher.Sum(nil)) + } + b.hash.Store(h) + return h +} + +type SimSBundle struct { + Bundle *SBundle + // MevGasPrice = (total coinbase profit) / (gas used) + MevGasPrice *big.Int + Profit *big.Int +} + +func GetRefundConfig(body *BundleBody, signer Signer) ([]RefundConfig, error) { + if body.Tx != nil { + address, err := signer.Sender(body.Tx) + if err != nil { + return nil, err + } + return []RefundConfig{{Address: address, Percent: 100}}, nil + } + if bundle := body.Bundle; bundle != nil { + if len(bundle.Validity.RefundConfig) > 0 { + return bundle.Validity.RefundConfig, nil + } else { + if len(bundle.Body) == 0 { + return nil, ErrIncorrectRefundConfig + } + return GetRefundConfig(&bundle.Body[0], signer) + } + } + return nil, ErrIncorrectRefundConfig +} + +// UsedSBundle is a bundle that was used in the block building +type UsedSBundle struct { + Bundle *SBundle + Success bool +} diff --git a/core/types/transaction.go b/core/types/transaction.go index b16f6fc3ba..327aa5088a 100644 --- a/core/types/transaction.go +++ b/core/types/transaction.go @@ -19,9 +19,13 @@ package types import ( "bytes" "container/heap" + "crypto/sha256" + "encoding/binary" "errors" + "github.com/google/uuid" "io" "math/big" + "sort" "sync/atomic" "time" @@ -620,10 +624,90 @@ func (s TxByNonce) Len() int { return len(s) } func (s TxByNonce) Less(i, j int) bool { return s[i].Nonce() < s[j].Nonce() } func (s TxByNonce) Swap(i, j int) { s[i], s[j] = s[j], s[i] } +type _Order interface { + AsTx() *Transaction + AsBundle() *SimulatedBundle + AsSBundle() *SimSBundle +} + +type _TxOrder struct { + tx *Transaction +} + +func (o _TxOrder) AsTx() *Transaction { return o.tx } +func (o _TxOrder) AsBundle() *SimulatedBundle { return nil } +func (o _TxOrder) AsSBundle() *SimSBundle { return nil } + +type _BundleOrder struct { + bundle *SimulatedBundle +} + +func (o _BundleOrder) AsTx() *Transaction { return nil } +func (o _BundleOrder) AsBundle() *SimulatedBundle { return o.bundle } +func (o _BundleOrder) AsSBundle() *SimSBundle { return nil } + +type _SBundleOrder struct { + sbundle *SimSBundle +} + +func (o _SBundleOrder) AsTx() *Transaction { return nil } +func (o _SBundleOrder) AsBundle() *SimulatedBundle { return nil } +func (o _SBundleOrder) AsSBundle() *SimSBundle { return o.sbundle } + // TxWithMinerFee wraps a transaction with its gas price or effective miner gasTipCap type TxWithMinerFee struct { - tx *Transaction - minerFee *uint256.Int + order _Order + minerFee *big.Int +} + +func (t *TxWithMinerFee) Tx() *Transaction { + return t.order.AsTx() +} + +func (t *TxWithMinerFee) Bundle() *SimulatedBundle { + return t.order.AsBundle() +} + +func (t *TxWithMinerFee) SBundle() *SimSBundle { + return t.order.AsSBundle() +} + +func (t *TxWithMinerFee) Price() *big.Int { + return new(big.Int).Set(t.minerFee) +} + +func (t *TxWithMinerFee) Profit(baseFee *big.Int, gasUsed uint64) *big.Int { + if tx := t.Tx(); tx != nil { + profit := new(big.Int).Sub(tx.GasPrice(), baseFee) + if gasUsed != 0 { + profit.Mul(profit, new(big.Int).SetUint64(gasUsed)) + } else { + profit.Mul(profit, new(big.Int).SetUint64(tx.Gas())) + } + return profit + } else if bundle := t.Bundle(); bundle != nil { + return bundle.EthSentToCoinbase + } else if sbundle := t.SBundle(); sbundle != nil { + return sbundle.Profit + } else { + panic("profit called on unsupported order type") + } +} + +// SetPrice sets the miner fee of the wrapped transaction. +func (t *TxWithMinerFee) SetPrice(price *big.Int) { + t.minerFee.Set(price) +} + +// SetProfit sets the profit of the wrapped transaction. +func (t *TxWithMinerFee) SetProfit(profit *big.Int) { + if bundle := t.Bundle(); bundle != nil { + bundle.TotalEth.Set(profit) + } else if sbundle := t.SBundle(); sbundle != nil { + sbundle.Profit.Set(profit) + } else { + panic("SetProfit called on unsupported order type") + } } // NewTxWithMinerFee creates a wrapped transaction, calculating the effective @@ -636,7 +720,25 @@ func NewTxWithMinerFee(tx *Transaction, baseFee *uint256.Int) (*TxWithMinerFee, } return &TxWithMinerFee{ - tx: tx, + order: _TxOrder{tx}, + minerFee: minerFee.ToBig(), + }, nil +} + +// NewBundleWithMinerFee creates a wrapped bundle. +func NewBundleWithMinerFee(bundle *SimulatedBundle, _ *big.Int) (*TxWithMinerFee, error) { + minerFee := bundle.MevGasPrice + return &TxWithMinerFee{ + order: _BundleOrder{bundle}, + minerFee: minerFee, + }, nil +} + +// NewSBundleWithMinerFee creates a wrapped bundle. +func NewSBundleWithMinerFee(sbundle *SimSBundle, _ *big.Int) (*TxWithMinerFee, error) { + minerFee := sbundle.MevGasPrice + return &TxWithMinerFee{ + order: _SBundleOrder{sbundle}, minerFee: minerFee, }, nil } @@ -651,9 +753,16 @@ func (s TxByPriceAndTime) Less(i, j int) bool { // deterministic sorting cmp := s[i].minerFee.Cmp(s[j].minerFee) if cmp == 0 { - return s[i].tx.time.Before(s[j].tx.time) - } + if s[i].Tx() != nil && s[j].Tx() != nil { + return s[i].Tx().time.Before(s[j].Tx().time) + } else if s[i].Bundle() != nil && s[j].Bundle() != nil { + return s[i].Bundle().TotalGasUsed <= s[j].Bundle().TotalGasUsed + } else if s[i].Bundle() != nil { + return false + } + return true + } return cmp > 0 } func (s TxByPriceAndTime) Swap(i, j int) { s[i], s[j] = s[j], s[i] } @@ -687,39 +796,25 @@ type TransactionsByPriceAndNonce struct { // // Note, the input map is reowned so the caller should not interact any more with // if after providing it to the constructor. -/* -func NewTransactionsByPriceAndNonce(signer Signer, txs map[common.Address]Transactions, baseFee *big.Int) *TransactionsByPriceAndNonce { +func NewTransactionsByPriceAndNonce(signer Signer, txs map[common.Address]Transactions, bundles []SimulatedBundle, sbundles []*SimSBundle, baseFee *uint256.Int) *TransactionsByPriceAndNonce { // Initialize a price and received time based heap with the head transactions - heads := make(TxByPriceAndTime, 0, len(txs)) - for from, accTxs := range txs { - if len(accTxs) == 0 { - continue - } + heads := make(TxByPriceAndTime, 0, len(txs)+len(bundles)+len(sbundles)) - acc, _ := Sender(signer, accTxs[0]) - wrapped, err := NewTxWithMinerFee(accTxs[0], baseFee) - // Remove transaction if sender doesn't match from, or if wrapping fails. - if acc != from || err != nil { - delete(txs, from) + for i := range sbundles { + wrapped, err := NewSBundleWithMinerFee(sbundles[i], baseFee.ToBig()) + if err != nil { continue } heads = append(heads, wrapped) - txs[from] = accTxs[1:] } - heap.Init(&heads) - // Assemble and return the transaction set - return &TransactionsByPriceAndNonce{ - txs: txs, - heads: heads, - signer: signer, - baseFee: baseFee, + for i := range bundles { + wrapped, err := NewBundleWithMinerFee(&bundles[i], baseFee.ToBig()) + if err != nil { + continue + } + heads = append(heads, wrapped) } -}*/ - -func NewTransactionsByPriceAndNonce(signer Signer, txs map[common.Address]Transactions, baseFee *uint256.Int) *TransactionsByPriceAndNonce { - // Initialize a price and received time based heap with the head transactions - heads := make(TxByPriceAndTime, 0, len(txs)) for from, accTxs := range txs { if len(accTxs) == 0 { @@ -750,30 +845,73 @@ func NewTransactionsByPriceAndNonce(signer Signer, txs map[common.Address]Transa } } +func (t *TransactionsByPriceAndNonce) DeepCopy() *TransactionsByPriceAndNonce { + newT := &TransactionsByPriceAndNonce{ + txs: make(map[common.Address]Transactions), + heads: append(TxByPriceAndTime{}, t.heads...), + signer: t.signer, + baseFee: t.baseFee.Clone(), + } + for k, v := range t.txs { + newT.txs[k] = v + } + return newT +} + // Peek returns the next transaction by price. -func (t *TransactionsByPriceAndNonce) Peek() *Transaction { +func (t *TransactionsByPriceAndNonce) Peek() *TxWithMinerFee { if len(t.heads) == 0 { return nil } - return t.heads[0].tx + return t.heads[0] } // Shift replaces the current best head with the next one from the same account. func (t *TransactionsByPriceAndNonce) Shift() { - acc, _ := Sender(t.signer, t.heads[0].tx) - if txs, ok := t.txs[acc]; ok && len(txs) > 0 { - if wrapped, err := NewTxWithMinerFee(txs[0], t.baseFee); err == nil { - t.heads[0], t.txs[acc] = wrapped, txs[1:] - heap.Fix(&t.heads, 0) - - return + if tx := t.heads[0].Tx(); tx != nil { + acc, _ := Sender(t.signer, tx) + if txs, ok := t.txs[acc]; ok && len(txs) > 0 { + if wrapped, err := NewTxWithMinerFee(txs[0], t.baseFee); err == nil { + t.heads[0], t.txs[acc] = wrapped, txs[1:] + heap.Fix(&t.heads, 0) + + return + } } } heap.Pop(&t.heads) } +// ShiftAndPushByAccountForTx attempts to update the transaction list associated with a given account address +// based on the input transaction account. If the associated account exists and has additional transactions, +// the top of the transaction list is popped and pushed to the heap. +// Note that this operation should only be performed when the head transaction on the heap is different from the +// input transaction. This operation is useful in scenarios where the current best head transaction for an account +// was already popped from the heap and we want to process the next one from the same account. +func (t *TransactionsByPriceAndNonce) ShiftAndPushByAccountForTx(tx *Transaction) { + if tx == nil { + return + } + + acc, _ := Sender(t.signer, tx) + if txs, exists := t.txs[acc]; exists && len(txs) > 0 { + if wrapped, err := NewTxWithMinerFee(txs[0], t.baseFee); err == nil { + t.txs[acc] = txs[1:] + heap.Push(&t.heads, wrapped) + } + } +} + +func (t *TransactionsByPriceAndNonce) Push(tx *TxWithMinerFee) { + if tx == nil { + return + } + + heap.Push(&t.heads, tx) +} + func (t *TransactionsByPriceAndNonce) GetTxs() int { return len(t.txs) } @@ -795,3 +933,57 @@ func copyAddressPtr(a *common.Address) *common.Address { return &cpy } + +var EmptyUUID uuid.UUID + +type LatestUuidBundle struct { + Uuid uuid.UUID + SigningAddress common.Address + BundleHash common.Hash + BundleUUID uuid.UUID +} + +type MevBundle struct { + Txs Transactions + BlockNumber *big.Int + Uuid uuid.UUID + SigningAddress common.Address + MinTimestamp uint64 + MaxTimestamp uint64 + RevertingTxHashes []common.Hash + Hash common.Hash +} + +func (b *MevBundle) UniquePayload() []byte { + var buf []byte + buf = binary.AppendVarint(buf, b.BlockNumber.Int64()) + buf = append(buf, b.Hash[:]...) + sort.Slice(b.RevertingTxHashes, func(i, j int) bool { + return bytes.Compare(b.RevertingTxHashes[i][:], b.RevertingTxHashes[j][:]) <= 0 + }) + for _, txHash := range b.RevertingTxHashes { + buf = append(buf, txHash[:]...) + } + return buf +} + +func (b *MevBundle) ComputeUUID() uuid.UUID { + return uuid.NewHash(sha256.New(), uuid.Nil, b.UniquePayload(), 5) +} + +func (b *MevBundle) RevertingHash(hash common.Hash) bool { + for _, revHash := range b.RevertingTxHashes { + if revHash == hash { + return true + } + } + return false +} + +type SimulatedBundle struct { + MevGasPrice *big.Int + TotalEth *big.Int + EthSentToCoinbase *big.Int + TotalGasUsed uint64 + OriginalBundle MevBundle +} diff --git a/eth/backend.go b/eth/backend.go index 2f64f1671f..3e281d6ae4 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -406,6 +406,10 @@ func (s *Ethereum) APIs() []rpc.API { }...) } +func (s *Ethereum) RegisterBundleFetcher(fetcher txpool.IFetcher) { + s.txPool.RegisterBundleFetcher(fetcher) +} + func (s *Ethereum) ResetWithGenesisBlock(gb *types.Block) { s.blockchain.ResetWithGenesisBlock(gb) } diff --git a/miner/algo_common.go b/miner/algo_common.go new file mode 100644 index 0000000000..cdea025b40 --- /dev/null +++ b/miner/algo_common.go @@ -0,0 +1,352 @@ +package miner + +import ( + "crypto/ecdsa" + "errors" + "fmt" + "math/big" + "sync/atomic" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/eth/tracers/logger" + "github.com/ethereum/go-ethereum/params" +) + +const ( + shiftTx = 1 + popTx = 2 +) + +const ( + // defaultProfitThresholdPercent is to ensure committed transactions, bundles, sbundles don't fall below this threshold + // when profit is enforced + defaultProfitThresholdPercent = 70 + + // defaultPriceCutoffPercent is for bucketing transactions by price, used for greedy buckets algorithm + defaultPriceCutoffPercent = 50 +) + +var ( + defaultAlgorithmConfig = algorithmConfig{ + DropRevertibleTxOnErr: false, + EnforceProfit: false, + ExpectedProfit: nil, + ProfitThresholdPercent: defaultProfitThresholdPercent, + PriceCutoffPercent: defaultPriceCutoffPercent, + } +) + +var emptyCodeHash = common.HexToHash("c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470") + +var ( + ErrMevGasPriceNotSet = errors.New("mev gas price not set") + errInterrupt = errors.New("miner worker interrupted") + errNoPrivateKey = errors.New("no private key provided") +) + +// lowProfitError is returned when an order is not committed due to low profit or low effective gas price +type lowProfitError struct { + ExpectedProfit *big.Int + ActualProfit *big.Int + + ExpectedEffectiveGasPrice *big.Int + ActualEffectiveGasPrice *big.Int +} + +func (e *lowProfitError) Error() string { + return fmt.Sprintf( + "low profit: expected %v, actual %v, expected effective gas price %v, actual effective gas price %v", + e.ExpectedProfit, e.ActualProfit, e.ExpectedEffectiveGasPrice, e.ActualEffectiveGasPrice, + ) +} + +type algorithmConfig struct { + // DropRevertibleTxOnErr is used when a revertible transaction has error on commit, and we wish to discard + // the transaction and continue processing the rest of a bundle or sbundle. + // Revertible transactions are specified as hashes that can revert in a bundle or sbundle. + DropRevertibleTxOnErr bool + // EnforceProfit is true if we want to enforce a minimum profit threshold + // for committing a transaction based on ProfitThresholdPercent + EnforceProfit bool + // ExpectedProfit should be set on a per-transaction basis when profit is enforced + ExpectedProfit *big.Int + // ProfitThresholdPercent is the minimum profit threshold for committing a transaction + ProfitThresholdPercent int // 0-100, e.g. 70 means 70% + // PriceCutoffPercent is the minimum effective gas price threshold used for bucketing transactions by price. + // For example if the top transaction in a list has an effective gas price of 1000 wei and PriceCutoffPercent + // is 10 (i.e. 10%), then the minimum effective gas price included in the same bucket as the top transaction + // is (1000 * 10%) = 100 wei. + PriceCutoffPercent int +} + +type chainData struct { + chainConfig *params.ChainConfig + chain *core.BlockChain + blacklist map[common.Address]struct{} +} + +// PayoutTransactionParams holds parameters for committing a payout transaction, used in commitPayoutTx +type PayoutTransactionParams struct { + Amount *big.Int + BaseFee *big.Int + ChainData chainData + Gas uint64 + CommitFn CommitTxFunc + Receiver common.Address + Sender common.Address + SenderBalance *big.Int + SenderNonce uint64 + Signer types.Signer + PrivateKey *ecdsa.PrivateKey +} + +type ( + // BuildBlockFunc is the function signature for building a block + BuildBlockFunc func( + simBundles []types.SimulatedBundle, + simSBundles []*types.SimSBundle, + transactions map[common.Address]types.Transactions) (*environment, []types.SimulatedBundle, []types.UsedSBundle) + + // CommitTxFunc is the function signature for committing a transaction + CommitTxFunc func(*types.Transaction, chainData) (*types.Receipt, int, error) +) + +func ValidateGasPriceAndProfit(algoConf algorithmConfig, actualPrice, expectedPrice *big.Int, tolerablePriceDifferencePercent int, + actualProfit, expectedProfit *big.Int) error { + // allow tolerablePriceDifferencePercent % divergence + expectedPriceMultiple := new(big.Int).Mul(expectedPrice, big.NewInt(100-int64(tolerablePriceDifferencePercent))) + actualPriceMultiple := new(big.Int).Mul(actualPrice, common.Big100) + + var errLowProfit *lowProfitError = nil + if expectedPriceMultiple.Cmp(actualPriceMultiple) > 0 { + errLowProfit = &lowProfitError{ + ExpectedEffectiveGasPrice: expectedPrice, + ActualEffectiveGasPrice: actualPrice, + } + } + + if algoConf.EnforceProfit { + // We want to make expected profit smaller to allow for some leeway in cases where the actual profit is + // lower due to transaction ordering + expectedProfitMultiple := common.PercentOf(expectedProfit, algoConf.ProfitThresholdPercent) + actualProfitMultiple := new(big.Int).Mul(actualProfit, common.Big100) + + if expectedProfitMultiple.Cmp(actualProfitMultiple) > 0 { + if errLowProfit == nil { + errLowProfit = new(lowProfitError) + } + errLowProfit.ExpectedProfit = expectedProfit + errLowProfit.ActualProfit = actualProfit + } + } + + if errLowProfit != nil { // staticcheck linter complains if we don't check for nil here + return errLowProfit + } + return nil +} + +func checkInterrupt(i *atomic.Int32) bool { + return i != nil && i.Load() != commitInterruptNone +} + +// Simulate bundle on top of current state without modifying it +// pending txs used to track if bundle tx is part of the mempool +func applyTransactionWithBlacklist( + signer types.Signer, config *params.ChainConfig, bc core.ChainContext, author *common.Address, gp *core.GasPool, + statedb *state.StateDB, header *types.Header, tx *types.Transaction, usedGas *uint64, + cfg vm.Config, blacklist map[common.Address]struct{}, +) (*types.Receipt, *state.StateDB, error) { + // short circuit if blacklist is empty + if len(blacklist) == 0 { + snap := statedb.Snapshot() + receipt, err := core.ApplyTransaction(config, bc, author, gp, statedb, header, tx, usedGas, cfg, nil) + if err != nil { + statedb.RevertToSnapshot(snap) + } + return receipt, statedb, err + } + + sender, err := types.Sender(signer, tx) + if err != nil { + return nil, statedb, err + } + + if _, in := blacklist[sender]; in { + return nil, statedb, errors.New("blacklist violation, tx.sender") + } + + if to := tx.To(); to != nil { + if _, in := blacklist[*to]; in { + return nil, statedb, errors.New("blacklist violation, tx.to") + } + } + + // we set precompile to nil, but they are set in the validation code + // there will be no difference in the result if precompile is not it the blocklist + touchTracer := logger.NewAccessListTracer(nil, common.Address{}, common.Address{}, nil) + cfg.Tracer = touchTracer + + // Let's ignore this + //hook := func() error { + // for _, accessTuple := range touchTracer.AccessList() { + // if _, in := blacklist[accessTuple.Address]; in { + // return errors.New("blacklist violation, tx trace") + // } + // } + // return nil + //} + + usedGasTmp := *usedGas + gasPoolTmp := new(core.GasPool).AddGas(gp.Gas()) + snap := statedb.Snapshot() + + receipt, err := core.ApplyTransaction(config, bc, author, gasPoolTmp, statedb, header, tx, &usedGasTmp, cfg, nil) + if err != nil { + statedb.RevertToSnapshot(snap) + return receipt, statedb, err + } + + *usedGas = usedGasTmp + *gp = *gasPoolTmp + return receipt, statedb, err +} + +func estimatePayoutTxGas(env *environment, sender, receiver common.Address, prv *ecdsa.PrivateKey, chData chainData) (uint64, bool, error) { + if codeHash := env.state.GetCodeHash(receiver); codeHash == (common.Hash{}) || codeHash == emptyCodeHash { + return params.TxGas, true, nil + } + gasLimit := env.gasPool.Gas() + + balance := new(big.Int).SetBytes([]byte{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}) + value := new(big.Int).SetBytes([]byte{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}) + + diff := newEnvironmentDiff(env) + diff.state.SetBalance(sender, balance) + receipt, err := diff.commitPayoutTx(value, sender, receiver, gasLimit, prv, chData) + if err != nil { + return 0, false, err + } + return receipt.GasUsed, false, nil +} + +func applyPayoutTx(envDiff *environmentDiff, sender, receiver common.Address, gas uint64, amountWithFees *big.Int, prv *ecdsa.PrivateKey, chData chainData) (*types.Receipt, error) { + amount := new(big.Int).Sub(amountWithFees, new(big.Int).Mul(envDiff.header.BaseFee, big.NewInt(int64(gas)))) + + if amount.Sign() < 0 { + return nil, errors.New("not enough funds available") + } + rec, err := envDiff.commitPayoutTx(amount, sender, receiver, gas, prv, chData) + if err != nil { + return nil, fmt.Errorf("failed to commit payment tx: %w", err) + } else if rec.Status != types.ReceiptStatusSuccessful { + return nil, fmt.Errorf("payment tx failed") + } + return rec, nil +} + +func commitPayoutTx(parameters PayoutTransactionParams) (*types.Receipt, error) { + if parameters.Gas < params.TxGas { + return nil, errors.New("not enough gas for intrinsic gas cost") + } + + requiredBalance := new(big.Int).Mul(parameters.BaseFee, new(big.Int).SetUint64(parameters.Gas)) + requiredBalance = requiredBalance.Add(requiredBalance, parameters.Amount) + if requiredBalance.Cmp(parameters.SenderBalance) > 0 { + return nil, errors.New("not enough balance") + } + + tx, err := types.SignNewTx(parameters.PrivateKey, parameters.Signer, &types.DynamicFeeTx{ + ChainID: parameters.ChainData.chainConfig.ChainID, + Nonce: parameters.SenderNonce, + GasTipCap: new(big.Int), + GasFeeCap: parameters.BaseFee, + Gas: parameters.Gas, + To: ¶meters.Receiver, + Value: parameters.Amount, + }) + if err != nil { + return nil, err + } + + txSender, err := types.Sender(parameters.Signer, tx) + if err != nil { + return nil, err + } + + if txSender != parameters.Sender { + return nil, errors.New("incorrect sender private key") + } + + receipt, _, err := parameters.CommitFn(tx, parameters.ChainData) + return receipt, err +} + +func insertPayoutTx(env *environment, sender, receiver common.Address, gas uint64, isEOA bool, availableFunds *big.Int, prv *ecdsa.PrivateKey, chData chainData) (*types.Receipt, error) { + if isEOA { + diff := newEnvironmentDiff(env) + rec, err := applyPayoutTx(diff, sender, receiver, gas, availableFunds, prv, chData) + if err != nil { + return nil, err + } + diff.applyToBaseEnv() + return rec, nil + } + + var err error + for i := 0; i < 6; i++ { + diff := newEnvironmentDiff(env) + var rec *types.Receipt + rec, err = applyPayoutTx(diff, sender, receiver, gas, availableFunds, prv, chData) + if err != nil { + gas += 1000 + continue + } + + if gas == rec.GasUsed { + diff.applyToBaseEnv() + return rec, nil + } + + exactEnvDiff := newEnvironmentDiff(env) + exactRec, err := applyPayoutTx(exactEnvDiff, sender, receiver, rec.GasUsed, availableFunds, prv, chData) + if err != nil { + diff.applyToBaseEnv() + return rec, nil + } + exactEnvDiff.applyToBaseEnv() + return exactRec, nil + } + + if err == nil { + return nil, errors.New("could not estimate gas") + } + + return nil, err +} + +// CheckRetryOrderAndReinsert checks if the order has been retried up to the retryLimit and if not, reinserts the order into the orders heap. +func CheckRetryOrderAndReinsert( + order *types.TxWithMinerFee, orders *types.TransactionsByPriceAndNonce, + retryMap map[*types.TxWithMinerFee]int, retryLimit int) bool { + var isRetryable bool = false + if retryCount, exists := retryMap[order]; exists { + if retryCount != retryLimit { + isRetryable = true + retryMap[order] = retryCount + 1 + } + } else { + retryMap[order] = 0 + isRetryable = true + } + + if isRetryable { + orders.Push(order) + } + + return isRetryable +} diff --git a/miner/algo_greedy.go b/miner/algo_greedy.go new file mode 100644 index 0000000000..7b5df5ff48 --- /dev/null +++ b/miner/algo_greedy.go @@ -0,0 +1,114 @@ +package miner + +import ( + "crypto/ecdsa" + "github.com/holiman/uint256" + "sync/atomic" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/params" +) + +// / To use it: +// / 1. Copy relevant data from the worker +// / 2. Call buildBlock +// / 2. If new bundles, txs arrive, call buildBlock again +// / This struct lifecycle is tied to 1 block-building task +type greedyBuilder struct { + inputEnvironment *environment + chainData chainData + builderKey *ecdsa.PrivateKey + interrupt *atomic.Int32 + algoConf algorithmConfig +} + +func newGreedyBuilder( + chain *core.BlockChain, chainConfig *params.ChainConfig, algoConf *algorithmConfig, + blacklist map[common.Address]struct{}, env *environment, key *ecdsa.PrivateKey, interrupt *atomic.Int32, +) *greedyBuilder { + if algoConf == nil { + panic("algoConf cannot be nil") + } + + return &greedyBuilder{ + inputEnvironment: env, + chainData: chainData{chainConfig, chain, blacklist}, + builderKey: key, + interrupt: interrupt, + algoConf: *algoConf, + } +} + +func (b *greedyBuilder) mergeOrdersIntoEnvDiff( + envDiff *environmentDiff, orders *types.TransactionsByPriceAndNonce) ([]types.SimulatedBundle, []types.UsedSBundle, +) { + var ( + usedBundles []types.SimulatedBundle + usedSbundles []types.UsedSBundle + ) + for { + order := orders.Peek() + if order == nil { + break + } + + if tx := order.Tx(); tx != nil { + receipt, skip, err := envDiff.commitTx(tx, b.chainData) + switch skip { + case shiftTx: + orders.Shift() + case popTx: + orders.Pop() + } + + if err != nil { + log.Trace("could not apply tx", "hash", tx.Hash(), "err", err) + continue + } + effGapPrice, err := tx.EffectiveGasTip(envDiff.baseEnvironment.header.BaseFee) + if err == nil { + log.Trace("Included tx", "EGP", effGapPrice.String(), "gasUsed", receipt.GasUsed) + } + } else if bundle := order.Bundle(); bundle != nil { + //log.Debug("buildBlock considering bundle", "egp", bundle.MevGasPrice.String(), "hash", bundle.OriginalBundle.Hash) + err := envDiff.commitBundle(bundle, b.chainData, b.interrupt, b.algoConf) + orders.Pop() + if err != nil { + log.Trace("Could not apply bundle", "bundle", bundle.OriginalBundle.Hash, "err", err) + continue + } + + log.Trace("Included bundle", "bundleEGP", bundle.MevGasPrice.String(), "gasUsed", bundle.TotalGasUsed, "ethToCoinbase", ethIntToFloat(bundle.TotalEth)) + usedBundles = append(usedBundles, *bundle) + } else if sbundle := order.SBundle(); sbundle != nil { + usedEntry := types.UsedSBundle{ + Bundle: sbundle.Bundle, + } + err := envDiff.commitSBundle(sbundle, b.chainData, b.interrupt, b.builderKey, b.algoConf) + orders.Pop() + if err != nil { + log.Trace("Could not apply sbundle", "bundle", sbundle.Bundle.Hash(), "err", err) + usedEntry.Success = false + usedSbundles = append(usedSbundles, usedEntry) + continue + } + + log.Trace("Included sbundle", "bundleEGP", sbundle.MevGasPrice.String(), "ethToCoinbase", ethIntToFloat(sbundle.Profit)) + usedEntry.Success = true + usedSbundles = append(usedSbundles, usedEntry) + } + } + return usedBundles, usedSbundles +} + +func (b *greedyBuilder) buildBlock(simBundles []types.SimulatedBundle, simSBundles []*types.SimSBundle, transactions map[common.Address]types.Transactions) (*environment, []types.SimulatedBundle, []types.UsedSBundle) { + baseFee, _ := uint256.FromBig(b.inputEnvironment.header.BaseFee) + orders := types.NewTransactionsByPriceAndNonce(b.inputEnvironment.signer, transactions, simBundles, simSBundles, baseFee) + envDiff := newEnvironmentDiff(b.inputEnvironment.copy()) + usedBundles, usedSbundles := b.mergeOrdersIntoEnvDiff(envDiff, orders) + envDiff.applyToBaseEnv() + return envDiff.baseEnvironment, usedBundles, usedSbundles +} diff --git a/miner/algo_greedy_buckets.go b/miner/algo_greedy_buckets.go new file mode 100644 index 0000000000..a0c2b36460 --- /dev/null +++ b/miner/algo_greedy_buckets.go @@ -0,0 +1,228 @@ +package miner + +import ( + "crypto/ecdsa" + "errors" + "github.com/holiman/uint256" + "math/big" + "sort" + "sync/atomic" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/params" +) + +// / To use it: +// / 1. Copy relevant data from the worker +// / 2. Call buildBlock +// / 2. If new bundles, txs arrive, call buildBlock again +// / This struct lifecycle is tied to 1 block-building task +type greedyBucketsBuilder struct { + inputEnvironment *environment + chainData chainData + builderKey *ecdsa.PrivateKey + interrupt *atomic.Int32 + gasUsedMap map[*types.TxWithMinerFee]uint64 + algoConf algorithmConfig +} + +func newGreedyBucketsBuilder( + chain *core.BlockChain, chainConfig *params.ChainConfig, algoConf *algorithmConfig, + blacklist map[common.Address]struct{}, env *environment, key *ecdsa.PrivateKey, interrupt *atomic.Int32, +) *greedyBucketsBuilder { + if algoConf == nil { + panic("algoConf cannot be nil") + } + + return &greedyBucketsBuilder{ + inputEnvironment: env, + chainData: chainData{chainConfig: chainConfig, chain: chain, blacklist: blacklist}, + builderKey: key, + interrupt: interrupt, + gasUsedMap: make(map[*types.TxWithMinerFee]uint64), + algoConf: *algoConf, + } +} + +// CutoffPriceFromOrder returns the cutoff price for a given order based on the cutoff percent. +// For example, if the cutoff percent is 90, the cutoff price will be 90% of the order price, rounded down to the nearest integer. +func CutoffPriceFromOrder(order *types.TxWithMinerFee, cutoffPercent int) *big.Int { + return common.PercentOf(order.Price(), cutoffPercent) +} + +// IsOrderInPriceRange returns true if the order price is greater than or equal to the minPrice. +func IsOrderInPriceRange(order *types.TxWithMinerFee, minPrice *big.Int) bool { + return order.Price().Cmp(minPrice) >= 0 +} + +func (b *greedyBucketsBuilder) commit(envDiff *environmentDiff, + transactions []*types.TxWithMinerFee, + orders *types.TransactionsByPriceAndNonce, + gasUsedMap map[*types.TxWithMinerFee]uint64, retryMap map[*types.TxWithMinerFee]int, retryLimit int, +) ([]types.SimulatedBundle, []types.UsedSBundle) { + var ( + algoConf = b.algoConf + + usedBundles []types.SimulatedBundle + usedSbundles []types.UsedSBundle + ) + + for _, order := range transactions { + if tx := order.Tx(); tx != nil { + receipt, skip, err := envDiff.commitTx(tx, b.chainData) + if err != nil { + log.Trace("could not apply tx", "hash", tx.Hash(), "err", err) + + // attempt to retry transaction commit up to retryLimit + // the gas used is set for the order to re-calculate profit of the transaction for subsequent retries + if receipt != nil { + // if the receipt is nil we don't attempt to retry the transaction - this is to mitigate abuse since + // without a receipt the default profit calculation for a transaction uses the gas limit which + // can cause the transaction to always be first in any profit-sorted transaction list + gasUsedMap[order] = receipt.GasUsed + CheckRetryOrderAndReinsert(order, orders, retryMap, retryLimit) + } + continue + } + + if skip == shiftTx { + orders.ShiftAndPushByAccountForTx(tx) + } + + effGapPrice, err := tx.EffectiveGasTip(envDiff.baseEnvironment.header.BaseFee) + if err == nil { + log.Trace("Included tx", "EGP", effGapPrice.String(), "gasUsed", receipt.GasUsed) + } + } else if bundle := order.Bundle(); bundle != nil { + err := envDiff.commitBundle(bundle, b.chainData, b.interrupt, algoConf) + if err != nil { + log.Trace("Could not apply bundle", "bundle", bundle.OriginalBundle.Hash, "err", err) + + var e *lowProfitError + if errors.As(err, &e) { + if e.ActualEffectiveGasPrice != nil { + order.SetPrice(e.ActualEffectiveGasPrice) + } + + if e.ActualProfit != nil { + order.SetProfit(e.ActualProfit) + } + // if the bundle was not included due to low profit, we can retry the bundle + CheckRetryOrderAndReinsert(order, orders, retryMap, retryLimit) + } + continue + } + + log.Trace("Included bundle", "bundleEGP", bundle.MevGasPrice.String(), + "gasUsed", bundle.TotalGasUsed, "ethToCoinbase", ethIntToFloat(bundle.EthSentToCoinbase)) + usedBundles = append(usedBundles, *bundle) + } else if sbundle := order.SBundle(); sbundle != nil { + usedEntry := types.UsedSBundle{ + Bundle: sbundle.Bundle, + } + err := envDiff.commitSBundle(sbundle, b.chainData, b.interrupt, b.builderKey, algoConf) + if err != nil { + log.Trace("Could not apply sbundle", "bundle", sbundle.Bundle.Hash(), "err", err) + + var e *lowProfitError + if errors.As(err, &e) { + if e.ActualEffectiveGasPrice != nil { + order.SetPrice(e.ActualEffectiveGasPrice) + } + + if e.ActualProfit != nil { + order.SetProfit(e.ActualProfit) + } + + // if the sbundle was not included due to low profit, we can retry the bundle + if ok := CheckRetryOrderAndReinsert(order, orders, retryMap, retryLimit); !ok { + usedEntry.Success = false + usedSbundles = append(usedSbundles, usedEntry) + } + } + continue + } + + log.Trace("Included sbundle", "bundleEGP", sbundle.MevGasPrice.String(), "ethToCoinbase", ethIntToFloat(sbundle.Profit)) + usedEntry.Success = true + usedSbundles = append(usedSbundles, usedEntry) + } else { + // note: this should never happen because we should not be inserting invalid transaction types into + // the orders heap + panic("unsupported order type found") + } + } + return usedBundles, usedSbundles +} + +func (b *greedyBucketsBuilder) mergeOrdersIntoEnvDiff( + envDiff *environmentDiff, orders *types.TransactionsByPriceAndNonce) ([]types.SimulatedBundle, []types.UsedSBundle, +) { + if orders.Peek() == nil { + return nil, nil + } + + const retryLimit = 1 + + var ( + baseFee = envDiff.baseEnvironment.header.BaseFee + retryMap = make(map[*types.TxWithMinerFee]int) + usedBundles []types.SimulatedBundle + usedSbundles []types.UsedSBundle + transactions []*types.TxWithMinerFee + priceCutoffPercent = b.algoConf.PriceCutoffPercent + + SortInPlaceByProfit = func(baseFee *big.Int, transactions []*types.TxWithMinerFee, gasUsedMap map[*types.TxWithMinerFee]uint64) { + sort.SliceStable(transactions, func(i, j int) bool { + return transactions[i].Profit(baseFee, gasUsedMap[transactions[i]]).Cmp(transactions[j].Profit(baseFee, gasUsedMap[transactions[j]])) > 0 + }) + } + ) + + minPrice := CutoffPriceFromOrder(orders.Peek(), priceCutoffPercent) + for { + order := orders.Peek() + if order == nil { + if len(transactions) != 0 { + SortInPlaceByProfit(baseFee, transactions, b.gasUsedMap) + bundles, sbundles := b.commit(envDiff, transactions, orders, b.gasUsedMap, retryMap, retryLimit) + usedBundles = append(usedBundles, bundles...) + usedSbundles = append(usedSbundles, sbundles...) + transactions = nil + // re-run since committing transactions may have pushed higher nonce transactions, or previously + // failed transactions back into orders heap + continue + } + break + } + + if ok := IsOrderInPriceRange(order, minPrice); ok { + orders.Pop() + transactions = append(transactions, order) + } else { + if len(transactions) != 0 { + SortInPlaceByProfit(baseFee, transactions, b.gasUsedMap) + bundles, sbundles := b.commit(envDiff, transactions, orders, b.gasUsedMap, retryMap, retryLimit) + usedBundles = append(usedBundles, bundles...) + usedSbundles = append(usedSbundles, sbundles...) + transactions = nil + } + minPrice = CutoffPriceFromOrder(order, priceCutoffPercent) + } + } + + return usedBundles, usedSbundles +} + +func (b *greedyBucketsBuilder) buildBlock(simBundles []types.SimulatedBundle, simSBundles []*types.SimSBundle, transactions map[common.Address]types.Transactions) (*environment, []types.SimulatedBundle, []types.UsedSBundle) { + baseFee, _ := uint256.FromBig(b.inputEnvironment.header.BaseFee) + + orders := types.NewTransactionsByPriceAndNonce(b.inputEnvironment.signer, transactions, simBundles, simSBundles, baseFee) + envDiff := newEnvironmentDiff(b.inputEnvironment.copy()) + usedBundles, usedSbundles := b.mergeOrdersIntoEnvDiff(envDiff, orders) + envDiff.applyToBaseEnv() + return envDiff.baseEnvironment, usedBundles, usedSbundles +} diff --git a/miner/algo_greedy_buckets_multisnap.go b/miner/algo_greedy_buckets_multisnap.go new file mode 100644 index 0000000000..513c584f9b --- /dev/null +++ b/miner/algo_greedy_buckets_multisnap.go @@ -0,0 +1,244 @@ +package miner + +import ( + "crypto/ecdsa" + "errors" + "github.com/holiman/uint256" + "math/big" + "sort" + "sync/atomic" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/params" +) + +// / To use it: +// / 1. Copy relevant data from the worker +// / 2. Call buildBlock +// / 2. If new bundles, txs arrive, call buildBlock again +// / This struct lifecycle is tied to 1 block-building task +type greedyBucketsMultiSnapBuilder struct { + inputEnvironment *environment + chainData chainData + builderKey *ecdsa.PrivateKey + interrupt *atomic.Int32 + gasUsedMap map[*types.TxWithMinerFee]uint64 + algoConf algorithmConfig +} + +func newGreedyBucketsMultiSnapBuilder( + chain *core.BlockChain, chainConfig *params.ChainConfig, algoConf *algorithmConfig, + blacklist map[common.Address]struct{}, env *environment, key *ecdsa.PrivateKey, interrupt *atomic.Int32, +) *greedyBucketsMultiSnapBuilder { + if algoConf == nil { + panic("algoConf cannot be nil") + } + + return &greedyBucketsMultiSnapBuilder{ + inputEnvironment: env, + chainData: chainData{chainConfig: chainConfig, chain: chain, blacklist: blacklist}, + builderKey: key, + interrupt: interrupt, + gasUsedMap: make(map[*types.TxWithMinerFee]uint64), + algoConf: *algoConf, + } +} + +func (b *greedyBucketsMultiSnapBuilder) commit(changes *envChanges, + transactions []*types.TxWithMinerFee, + orders *types.TransactionsByPriceAndNonce, + gasUsedMap map[*types.TxWithMinerFee]uint64, retryMap map[*types.TxWithMinerFee]int, retryLimit int, +) ([]types.SimulatedBundle, []types.UsedSBundle) { + var ( + algoConf = b.algoConf + + usedBundles []types.SimulatedBundle + usedSbundles []types.UsedSBundle + ) + + for _, order := range transactions { + if err := changes.env.state.NewMultiTxSnapshot(); err != nil { + log.Error("Failed to create new multi-tx snapshot", "err", err) + return usedBundles, usedSbundles + } + + orderFailed := false + + if tx := order.Tx(); tx != nil { + receipt, skip, err := changes.commitTx(tx, b.chainData) + orderFailed = err != nil + if err != nil { + log.Trace("could not apply tx", "hash", tx.Hash(), "err", err) + + // attempt to retry transaction commit up to retryLimit + // the gas used is set for the order to re-calculate profit of the transaction for subsequent retries + if receipt != nil { + // if the receipt is nil we don't attempt to retry the transaction - this is to mitigate abuse since + // without a receipt the default profit calculation for a transaction uses the gas limit which + // can cause the transaction to always be first in any profit-sorted transaction list + gasUsedMap[order] = receipt.GasUsed + CheckRetryOrderAndReinsert(order, orders, retryMap, retryLimit) + } + } else { + if skip == shiftTx { + orders.ShiftAndPushByAccountForTx(tx) + } + // we don't check for error here because if EGP returns error, it would have been caught and returned by commitTx + effGapPrice, _ := tx.EffectiveGasTip(changes.env.header.BaseFee) + log.Trace("Included tx", "EGP", effGapPrice.String(), "gasUsed", receipt.GasUsed) + } + } else if bundle := order.Bundle(); bundle != nil { + err := changes.commitBundle(bundle, b.chainData, algoConf) + orderFailed = err != nil + if err != nil { + log.Trace("Could not apply bundle", "bundle", bundle.OriginalBundle.Hash, "err", err) + + var e *lowProfitError + if errors.As(err, &e) { + if e.ActualEffectiveGasPrice != nil { + order.SetPrice(e.ActualEffectiveGasPrice) + } + + if e.ActualProfit != nil { + order.SetProfit(e.ActualProfit) + } + // if the bundle was not included due to low profit, we can retry the bundle + CheckRetryOrderAndReinsert(order, orders, retryMap, retryLimit) + } + } else { + log.Trace("Included bundle", "bundleEGP", bundle.MevGasPrice.String(), + "gasUsed", bundle.TotalGasUsed, "ethToCoinbase", ethIntToFloat(bundle.EthSentToCoinbase)) + usedBundles = append(usedBundles, *bundle) + } + } else if sbundle := order.SBundle(); sbundle != nil { + err := changes.CommitSBundle(sbundle, b.chainData, b.builderKey, algoConf) + orderFailed = err != nil + usedEntry := types.UsedSBundle{ + Bundle: sbundle.Bundle, + Success: err == nil, + } + + isValidOrNotRetried := true + if err != nil { + log.Trace("Could not apply sbundle", "bundle", sbundle.Bundle.Hash(), "err", err) + + var e *lowProfitError + if errors.As(err, &e) { + if e.ActualEffectiveGasPrice != nil { + order.SetPrice(e.ActualEffectiveGasPrice) + } + + if e.ActualProfit != nil { + order.SetProfit(e.ActualProfit) + } + + // if the sbundle was not included due to low profit, we can retry the bundle + if ok := CheckRetryOrderAndReinsert(order, orders, retryMap, retryLimit); ok { + isValidOrNotRetried = false + } + } + } else { + log.Trace("Included sbundle", "bundleEGP", sbundle.MevGasPrice.String(), "ethToCoinbase", ethIntToFloat(sbundle.Profit)) + } + + if isValidOrNotRetried { + usedSbundles = append(usedSbundles, usedEntry) + } + } else { + // note: this should never happen because we should not be inserting invalid transaction types into + // the orders heap + panic("unsupported order type found") + } + + if orderFailed { + if err := changes.env.state.MultiTxSnapshotRevert(); err != nil { + log.Error("Failed to revert snapshot", "err", err) + return usedBundles, usedSbundles + } + } else { + if err := changes.env.state.MultiTxSnapshotCommit(); err != nil { + log.Error("Failed to commit snapshot", "err", err) + return usedBundles, usedSbundles + } + } + } + return usedBundles, usedSbundles +} + +func (b *greedyBucketsMultiSnapBuilder) mergeOrdersAndApplyToEnv( + orders *types.TransactionsByPriceAndNonce) (*environment, []types.SimulatedBundle, []types.UsedSBundle) { + if orders.Peek() == nil { + return b.inputEnvironment, nil, nil + } + + changes, err := newEnvChanges(b.inputEnvironment) + if err != nil { + log.Error("Failed to create new environment changes", "err", err) + return b.inputEnvironment, nil, nil + } + + const retryLimit = 1 + + var ( + baseFee = changes.env.header.BaseFee + retryMap = make(map[*types.TxWithMinerFee]int) + usedBundles []types.SimulatedBundle + usedSbundles []types.UsedSBundle + transactions []*types.TxWithMinerFee + priceCutoffPercent = b.algoConf.PriceCutoffPercent + + SortInPlaceByProfit = func(baseFee *big.Int, transactions []*types.TxWithMinerFee, gasUsedMap map[*types.TxWithMinerFee]uint64) { + sort.SliceStable(transactions, func(i, j int) bool { + return transactions[i].Profit(baseFee, gasUsedMap[transactions[i]]).Cmp(transactions[j].Profit(baseFee, gasUsedMap[transactions[j]])) > 0 + }) + } + ) + + minPrice := CutoffPriceFromOrder(orders.Peek(), priceCutoffPercent) + for { + order := orders.Peek() + if order == nil { + if len(transactions) != 0 { + SortInPlaceByProfit(baseFee, transactions, b.gasUsedMap) + bundles, sbundles := b.commit(changes, transactions, orders, b.gasUsedMap, retryMap, retryLimit) + usedBundles = append(usedBundles, bundles...) + usedSbundles = append(usedSbundles, sbundles...) + transactions = nil + // re-run since committing transactions may have pushed higher nonce transactions, or previously + // failed transactions back into orders heap + continue + } + break + } + + if ok := IsOrderInPriceRange(order, minPrice); ok { + orders.Pop() + transactions = append(transactions, order) + } else { + if len(transactions) != 0 { + SortInPlaceByProfit(baseFee, transactions, b.gasUsedMap) + bundles, sbundles := b.commit(changes, transactions, orders, b.gasUsedMap, retryMap, retryLimit) + usedBundles = append(usedBundles, bundles...) + usedSbundles = append(usedSbundles, sbundles...) + transactions = nil + } + minPrice = CutoffPriceFromOrder(order, priceCutoffPercent) + } + } + + if err := changes.apply(); err != nil { + log.Error("Failed to apply changes", "err", err) + return b.inputEnvironment, nil, nil + } + + return changes.env, usedBundles, usedSbundles +} + +func (b *greedyBucketsMultiSnapBuilder) buildBlock(simBundles []types.SimulatedBundle, simSBundles []*types.SimSBundle, transactions map[common.Address]types.Transactions) (*environment, []types.SimulatedBundle, []types.UsedSBundle) { + baseFee, _ := uint256.FromBig(b.inputEnvironment.header.BaseFee) + orders := types.NewTransactionsByPriceAndNonce(b.inputEnvironment.signer, transactions, simBundles, simSBundles, baseFee) + return b.mergeOrdersAndApplyToEnv(orders) +} diff --git a/miner/algo_greedy_multisnap.go b/miner/algo_greedy_multisnap.go new file mode 100644 index 0000000000..0f243612a4 --- /dev/null +++ b/miner/algo_greedy_multisnap.go @@ -0,0 +1,137 @@ +package miner + +import ( + "crypto/ecdsa" + "github.com/holiman/uint256" + "sync/atomic" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/params" +) + +// / To use it: +// / 1. Copy relevant data from the worker +// / 2. Call buildBlock +// / 2. If new bundles, txs arrive, call buildBlock again +// / This struct lifecycle is tied to 1 block-building task +type greedyMultiSnapBuilder struct { + inputEnvironment *environment + chainData chainData + builderKey *ecdsa.PrivateKey + interrupt *atomic.Int32 + algoConf algorithmConfig +} + +func newGreedyMultiSnapBuilder( + chain *core.BlockChain, chainConfig *params.ChainConfig, algoConf *algorithmConfig, + blacklist map[common.Address]struct{}, env *environment, key *ecdsa.PrivateKey, interrupt *atomic.Int32, +) *greedyMultiSnapBuilder { + if algoConf == nil { + algoConf = &defaultAlgorithmConfig + } + return &greedyMultiSnapBuilder{ + inputEnvironment: env, + chainData: chainData{chainConfig, chain, blacklist}, + builderKey: key, + interrupt: interrupt, + algoConf: *algoConf, + } +} + +func (b *greedyMultiSnapBuilder) buildBlock(simBundles []types.SimulatedBundle, simSBundles []*types.SimSBundle, transactions map[common.Address]types.Transactions) (*environment, []types.SimulatedBundle, []types.UsedSBundle) { + baseFee, _ := uint256.FromBig(b.inputEnvironment.header.BaseFee) + orders := types.NewTransactionsByPriceAndNonce(b.inputEnvironment.signer, transactions, simBundles, simSBundles, baseFee) + + var ( + usedBundles []types.SimulatedBundle + usedSbundles []types.UsedSBundle + ) + + changes, err := newEnvChanges(b.inputEnvironment) + if err != nil { + log.Error("Failed to create new environment changes", "err", err) + return b.inputEnvironment, usedBundles, usedSbundles + } + + for { + order := orders.Peek() + if order == nil { + break + } + + orderFailed := false + if err := changes.env.state.NewMultiTxSnapshot(); err != nil { + log.Error("Failed to create snapshot", "err", err) + return b.inputEnvironment, usedBundles, usedSbundles + } + + if tx := order.Tx(); tx != nil { + receipt, skip, err := changes.commitTx(tx, b.chainData) + switch skip { + case shiftTx: + orders.Shift() + case popTx: + orders.Pop() + } + orderFailed = err != nil + + if err != nil { + log.Trace("could not apply tx", "hash", tx.Hash(), "err", err) + } else { + // we don't check for error here because if EGP returns error, it would have been caught and returned by commitTx + effGapPrice, _ := tx.EffectiveGasTip(changes.env.header.BaseFee) + log.Trace("Included tx", "EGP", effGapPrice.String(), "gasUsed", receipt.GasUsed) + } + } else if bundle := order.Bundle(); bundle != nil { + err := changes.commitBundle(bundle, b.chainData, b.algoConf) + orders.Pop() + orderFailed = err != nil + + if err != nil { + log.Trace("Could not apply bundle", "bundle", bundle.OriginalBundle.Hash, "err", err) + } else { + log.Trace("Included bundle", "bundleEGP", bundle.MevGasPrice.String(), + "gasUsed", bundle.TotalGasUsed, "ethToCoinbase", ethIntToFloat(bundle.EthSentToCoinbase)) + usedBundles = append(usedBundles, *bundle) + } + } else if sbundle := order.SBundle(); sbundle != nil { + err := changes.CommitSBundle(sbundle, b.chainData, b.builderKey, b.algoConf) + orders.Pop() + orderFailed = err != nil + usedEntry := types.UsedSBundle{ + Bundle: sbundle.Bundle, + Success: err == nil, + } + + if err != nil { + log.Trace("Could not apply sbundle", "bundle", sbundle.Bundle.Hash(), "err", err) + } else { + log.Trace("Included sbundle", "bundleEGP", sbundle.MevGasPrice.String(), "ethToCoinbase", ethIntToFloat(sbundle.Profit)) + } + + usedSbundles = append(usedSbundles, usedEntry) + } + + if orderFailed { + if err := changes.env.state.MultiTxSnapshotRevert(); err != nil { + log.Error("Failed to revert snapshot", "err", err) + return b.inputEnvironment, usedBundles, usedSbundles + } + } else { + if err := changes.env.state.MultiTxSnapshotCommit(); err != nil { + log.Error("Failed to commit snapshot", "err", err) + return b.inputEnvironment, usedBundles, usedSbundles + } + } + } + + if err := changes.apply(); err != nil { + log.Error("Failed to apply changes", "err", err) + return b.inputEnvironment, usedBundles, usedSbundles + } + + return changes.env, usedBundles, usedSbundles +} diff --git a/miner/bundle_cache.go b/miner/bundle_cache.go new file mode 100644 index 0000000000..d1ff789b72 --- /dev/null +++ b/miner/bundle_cache.go @@ -0,0 +1,116 @@ +package miner + +import ( + "sync" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" +) + +const ( + maxHeaders = 3 +) + +type BundleCache struct { + mu sync.Mutex + entries []*BundleCacheEntry +} + +func NewBundleCache() *BundleCache { + return &BundleCache{ + entries: make([]*BundleCacheEntry, maxHeaders), + } +} + +func (b *BundleCache) GetBundleCache(header common.Hash) *BundleCacheEntry { + b.mu.Lock() + defer b.mu.Unlock() + + for _, entry := range b.entries { + if entry != nil && entry.headerHash == header { + return entry + } + } + newEntry := newCacheEntry(header) + b.entries = b.entries[1:] + b.entries = append(b.entries, newEntry) + + return newEntry +} + +type BundleCacheEntry struct { + mu sync.Mutex + headerHash common.Hash + successfulBundles map[common.Hash]*simulatedBundle + failedBundles map[common.Hash]struct{} + successfulSBundles map[common.Hash]*types.SimSBundle + failedSBundles map[common.Hash]struct{} +} + +func newCacheEntry(header common.Hash) *BundleCacheEntry { + return &BundleCacheEntry{ + headerHash: header, + successfulBundles: make(map[common.Hash]*simulatedBundle), + failedBundles: make(map[common.Hash]struct{}), + successfulSBundles: make(map[common.Hash]*types.SimSBundle), + failedSBundles: make(map[common.Hash]struct{}), + } +} + +func (c *BundleCacheEntry) GetSimulatedBundle(bundle common.Hash) (*types.SimulatedBundle, bool) { + c.mu.Lock() + defer c.mu.Unlock() + + if simmed, ok := c.successfulBundles[bundle]; ok { + return simmed, true + } + + if _, ok := c.failedBundles[bundle]; ok { + return nil, true + } + + return nil, false +} + +func (c *BundleCacheEntry) UpdateSimulatedBundles(result []*types.SimulatedBundle, bundles []types.MevBundle) { + c.mu.Lock() + defer c.mu.Unlock() + + for i, simBundle := range result { + bundleHash := bundles[i].Hash + if simBundle != nil { + c.successfulBundles[bundleHash] = simBundle + } else { + c.failedBundles[bundleHash] = struct{}{} + } + } +} + +func (c *BundleCacheEntry) GetSimSBundle(bundle common.Hash) (*types.SimSBundle, bool) { + c.mu.Lock() + defer c.mu.Unlock() + + if simmed, ok := c.successfulSBundles[bundle]; ok { + return simmed, true + } + + if _, ok := c.failedSBundles[bundle]; ok { + return nil, true + } + + return nil, false +} + +func (c *BundleCacheEntry) UpdateSimSBundle(result []*types.SimSBundle, bundles []*types.SBundle) { + c.mu.Lock() + defer c.mu.Unlock() + + for i, simBundle := range result { + bundleHash := bundles[i].Hash() + if simBundle != nil { + c.successfulSBundles[bundleHash] = simBundle + } else { + c.failedSBundles[bundleHash] = struct{}{} + } + } +} diff --git a/miner/env_changes.go b/miner/env_changes.go new file mode 100644 index 0000000000..3af63f0399 --- /dev/null +++ b/miner/env_changes.go @@ -0,0 +1,427 @@ +package miner + +import ( + "crypto/ecdsa" + "errors" + "fmt" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/log" +) + +// envChanges is a helper struct to apply and discard changes to the environment +type envChanges struct { + env *environment + gasPool *core.GasPool + usedGas uint64 + profit *big.Int + txs []*types.Transaction + receipts []*types.Receipt +} + +func newEnvChanges(env *environment) (*envChanges, error) { + if err := env.state.NewMultiTxSnapshot(); err != nil { + return nil, err + } + + return &envChanges{ + env: env, + gasPool: new(core.GasPool).AddGas(env.gasPool.Gas()), + usedGas: env.header.GasUsed, + profit: new(big.Int).Set(env.profit), + txs: make([]*types.Transaction, 0), + receipts: make([]*types.Receipt, 0), + }, nil +} + +func (c *envChanges) commitPayoutTx( + amount *big.Int, sender, receiver common.Address, + gas uint64, prv *ecdsa.PrivateKey, chData chainData) (*types.Receipt, error) { + return commitPayoutTx(PayoutTransactionParams{ + Amount: amount, + BaseFee: c.env.header.BaseFee, + ChainData: chData, + Gas: gas, + CommitFn: c.commitTx, + Receiver: receiver, + Sender: sender, + SenderBalance: c.env.state.GetBalance(sender), + SenderNonce: c.env.state.GetNonce(sender), + Signer: c.env.signer, + PrivateKey: prv, + }) +} + +func (c *envChanges) commitTx(tx *types.Transaction, chData chainData) (*types.Receipt, int, error) { + signer := c.env.signer + from, err := types.Sender(signer, tx) + if err != nil { + return nil, popTx, err + } + + gasPrice, err := tx.EffectiveGasTip(c.env.header.BaseFee) + if err != nil { + return nil, shiftTx, err + } + + c.env.state.SetTxContext(tx.Hash(), c.env.tcount+len(c.txs)) + receipt, _, err := applyTransactionWithBlacklist(signer, chData.chainConfig, chData.chain, &c.env.coinbase, c.gasPool, c.env.state, c.env.header, tx, &c.usedGas, *chData.chain.GetVMConfig(), chData.blacklist) + if err != nil { + switch { + case errors.Is(err, core.ErrGasLimitReached): + // Pop the current out-of-gas transaction without shifting in the next from the account + log.Trace("Gas limit exceeded for current block", "sender", from) + return receipt, popTx, err + + case errors.Is(err, core.ErrNonceTooLow): + // New head notification data race between the transaction pool and miner, shift + log.Trace("Skipping transaction with low nonce", "sender", from, "nonce", tx.Nonce()) + return receipt, shiftTx, err + + case errors.Is(err, core.ErrNonceTooHigh): + // Reorg notification data race between the transaction pool and miner, skip account = + log.Trace("Skipping account with hight nonce", "sender", from, "nonce", tx.Nonce()) + return receipt, popTx, err + + case errors.Is(err, core.ErrTxTypeNotSupported): + // Pop the unsupported transaction without shifting in the next from the account + log.Trace("Skipping unsupported transaction type", "sender", from, "type", tx.Type()) + return receipt, popTx, err + + default: + // Strange error, discard the transaction and get the next in line (note, the + // nonce-too-high clause will prevent us from executing in vain). + log.Trace("Transaction failed, account skipped", "hash", tx.Hash(), "err", err) + return receipt, shiftTx, err + } + } + + c.profit = c.profit.Add(c.profit, new(big.Int).Mul(new(big.Int).SetUint64(receipt.GasUsed), gasPrice)) + c.txs = append(c.txs, tx) + c.receipts = append(c.receipts, receipt) + + return receipt, shiftTx, nil +} + +func (c *envChanges) commitBundle(bundle *types.SimulatedBundle, chData chainData, algoConf algorithmConfig) error { + var ( + profitBefore = new(big.Int).Set(c.profit) + coinbaseBefore = new(big.Int).Set(c.env.state.GetBalance(c.env.coinbase)) + gasUsedBefore = c.usedGas + gasPoolBefore = new(core.GasPool).AddGas(c.gasPool.Gas()) + txsBefore = c.txs[:] + receiptsBefore = c.receipts[:] + hasBaseFee = c.env.header.BaseFee != nil + + bundleErr error + ) + + for _, tx := range bundle.OriginalBundle.Txs { + txHash := tx.Hash() + // TODO: Checks for base fee and dynamic fee txs should be moved to the transaction pool, + // similar to mev-share bundles. See SBundlesPool.validateTx() for reference. + if hasBaseFee && tx.Type() == types.DynamicFeeTxType { + // Sanity check for extremely large numbers + if tx.GasFeeCap().BitLen() > 256 { + bundleErr = core.ErrFeeCapVeryHigh + break + } + if tx.GasTipCap().BitLen() > 256 { + bundleErr = core.ErrTipVeryHigh + break + } + + // Ensure gasFeeCap is greater than or equal to gasTipCap. + if tx.GasFeeCapIntCmp(tx.GasTipCap()) < 0 { + bundleErr = core.ErrTipAboveFeeCap + break + } + } + receipt, _, err := c.commitTx(tx, chData) + + switch { + case err != nil: + isRevertibleTx := bundle.OriginalBundle.RevertingHash(txHash) + // if drop enabled, and revertible tx has error on commit, we skip the transaction and continue with next one + if algoConf.DropRevertibleTxOnErr && isRevertibleTx { + log.Trace("Found error on commit for revertible tx, but discard on err is enabled so skipping.", + "tx", txHash, "err", err) + } else { + bundleErr = err + } + case receipt != nil: + if receipt.Status == types.ReceiptStatusFailed && !bundle.OriginalBundle.RevertingHash(txHash) { + // if transaction reverted and isn't specified as reverting hash, return error + log.Trace("Bundle tx failed", "bundle", bundle.OriginalBundle.Hash, "tx", txHash, "err", err) + bundleErr = errors.New("bundle tx revert") + } + case receipt == nil && err == nil: + // NOTE: The expectation is that a receipt is only nil if an error occurred. + // If there is no error but receipt is nil, there is likely a programming error. + bundleErr = errors.New("invalid receipt when no error occurred") + } + + if bundleErr != nil { + break + } + } + + if bundleErr != nil { + c.rollback(gasUsedBefore, gasPoolBefore, profitBefore, txsBefore, receiptsBefore) + return bundleErr + } + + if bundle.MevGasPrice == nil { + c.rollback(gasUsedBefore, gasPoolBefore, profitBefore, txsBefore, receiptsBefore) + return ErrMevGasPriceNotSet + } + + var ( + bundleProfit = new(big.Int).Sub(c.env.state.GetBalance(c.env.coinbase), coinbaseBefore) + gasUsed = c.usedGas - gasUsedBefore + + // EGP = Effective Gas Price (Profit / GasUsed) + simulatedEGP = new(big.Int).Set(bundle.MevGasPrice) + actualEGP *big.Int + tolerablePriceDifferencePercent = 1 + + simulatedBundleProfit = new(big.Int).Set(bundle.TotalEth) + actualBundleProfit = new(big.Int).Set(bundleProfit) + ) + + if gasUsed == 0 { + c.rollback(gasUsedBefore, gasPoolBefore, profitBefore, txsBefore, receiptsBefore) + return errors.New("bundle gas used is 0") + } else { + actualEGP = new(big.Int).Div(bundleProfit, big.NewInt(int64(gasUsed))) + } + + err := ValidateGasPriceAndProfit(algoConf, + actualEGP, simulatedEGP, tolerablePriceDifferencePercent, + actualBundleProfit, simulatedBundleProfit, + ) + if err != nil { + c.rollback(gasUsedBefore, gasPoolBefore, profitBefore, txsBefore, receiptsBefore) + return err + } + + c.profit.Add(profitBefore, bundleProfit) + return nil +} + +func (c *envChanges) CommitSBundle(sbundle *types.SimSBundle, chData chainData, key *ecdsa.PrivateKey, algoConf algorithmConfig) error { + // TODO: Suggestion for future improvement: instead of checking if key is nil, panic. + // Discussed with @Ruteri, see PR#90 for details: https://github.com/flashbots/builder/pull/90#discussion_r1285567550 + if key == nil { + return errNoPrivateKey + } + + var ( + coinbaseBefore = new(big.Int).Set(c.env.state.GetBalance(c.env.coinbase)) + gasPoolBefore = new(core.GasPool).AddGas(c.gasPool.Gas()) + gasBefore = c.usedGas + txsBefore = c.txs[:] + receiptsBefore = c.receipts[:] + profitBefore = new(big.Int).Set(c.profit) + ) + + if err := c.commitSBundle(sbundle.Bundle, chData, key, algoConf); err != nil { + c.rollback(gasBefore, gasPoolBefore, profitBefore, txsBefore, receiptsBefore) + return err + } + + var ( + coinbaseAfter = c.env.state.GetBalance(c.env.header.Coinbase) + gasAfter = c.usedGas + + coinbaseDelta = new(big.Int).Sub(coinbaseAfter, coinbaseBefore) + gasDelta = new(big.Int).SetUint64(gasAfter - gasBefore) + ) + if coinbaseDelta.Cmp(common.Big0) < 0 { + c.rollback(gasBefore, gasPoolBefore, profitBefore, txsBefore, receiptsBefore) + return errors.New("coinbase balance decreased") + } + + gotEGP := new(big.Int).Div(coinbaseDelta, gasDelta) + simEGP := new(big.Int).Set(sbundle.MevGasPrice) + + // allow > 1% difference + actualEGP := new(big.Int).Mul(gotEGP, common.Big100) + simulatedEGP := new(big.Int).Mul(simEGP, big.NewInt(99)) + + if simulatedEGP.Cmp(actualEGP) > 0 { + c.rollback(gasBefore, gasPoolBefore, profitBefore, txsBefore, receiptsBefore) + return &lowProfitError{ + ExpectedEffectiveGasPrice: simEGP, + ActualEffectiveGasPrice: gotEGP, + } + } + + if algoConf.EnforceProfit { + // if profit is enforced between simulation and actual commit, only allow >-1% divergence + simulatedProfit := new(big.Int).Set(sbundle.Profit) + actualProfit := new(big.Int).Set(coinbaseDelta) + + // We want to make simulated profit smaller to allow for some leeway in cases where the actual profit is + // lower due to transaction ordering + simulatedProfitMultiple := common.PercentOf(simulatedProfit, algoConf.ProfitThresholdPercent) + actualProfitMultiple := new(big.Int).Mul(actualProfit, common.Big100) + + if simulatedProfitMultiple.Cmp(actualProfitMultiple) > 0 { + log.Trace("Lower sbundle profit found after inclusion", "sbundle", sbundle.Bundle.Hash()) + c.rollback(gasBefore, gasPoolBefore, profitBefore, txsBefore, receiptsBefore) + return &lowProfitError{ + ExpectedProfit: simulatedProfit, + ActualProfit: actualProfit, + } + } + } + + return nil +} + +func (c *envChanges) commitSBundle(sbundle *types.SBundle, chData chainData, key *ecdsa.PrivateKey, algoConf algorithmConfig) error { + var ( + // check inclusion + minBlock = sbundle.Inclusion.BlockNumber + maxBlock = sbundle.Inclusion.MaxBlockNumber + ) + if current := c.env.header.Number.Uint64(); current < minBlock || current > maxBlock { + return fmt.Errorf("bundle inclusion block number out of range: %d <= %d <= %d", minBlock, current, maxBlock) + } + + var ( + // extract constraints into convenient format + refundIdx = make([]bool, len(sbundle.Body)) + refundPercents = make([]int, len(sbundle.Body)) + ) + for _, el := range sbundle.Validity.Refund { + refundIdx[el.BodyIdx] = true + refundPercents[el.BodyIdx] = el.Percent + } + + var ( + totalProfit *big.Int = new(big.Int) + refundableProfit *big.Int = new(big.Int) + + coinbaseDelta = new(big.Int) + coinbaseBefore *big.Int + ) + + // insert body and check it + for i, el := range sbundle.Body { + coinbaseDelta.Set(common.Big0) + coinbaseBefore = c.env.state.GetBalance(c.env.coinbase) + + if el.Tx != nil { + receipt, _, err := c.commitTx(el.Tx, chData) + if err != nil { + // if drop enabled, and revertible tx has error on commit, + // we skip the transaction and continue with next one + if algoConf.DropRevertibleTxOnErr && el.CanRevert { + log.Trace("Found error on commit for revertible tx, but discard on err is enabled so skipping.", + "tx", el.Tx.Hash(), "err", err) + continue + } + return err + } + if receipt.Status != types.ReceiptStatusSuccessful && !el.CanRevert { + return errors.New("tx failed") + } + } else if el.Bundle != nil { + err := c.commitSBundle(el.Bundle, chData, key, algoConf) + if err != nil { + return err + } + } else { + return errors.New("invalid body element") + } + + coinbaseDelta.Set(c.env.state.GetBalance(c.env.coinbase)) + coinbaseDelta.Sub(coinbaseDelta, coinbaseBefore) + + totalProfit.Add(totalProfit, coinbaseDelta) + if !refundIdx[i] { + refundableProfit.Add(refundableProfit, coinbaseDelta) + } + } + + // enforce constraints + coinbaseDelta.Set(common.Big0) + coinbaseBefore = c.env.state.GetBalance(c.env.header.Coinbase) + for i, el := range refundPercents { + if !refundIdx[i] { + continue + } + refundConfig, err := types.GetRefundConfig(&sbundle.Body[i], c.env.signer) + if err != nil { + return err + } + + maxPayoutCost := new(big.Int).Set(core.SbundlePayoutMaxCost) + maxPayoutCost.Mul(maxPayoutCost, big.NewInt(int64(len(refundConfig)))) + maxPayoutCost.Mul(maxPayoutCost, c.env.header.BaseFee) + + allocatedValue := common.PercentOf(refundableProfit, el) + allocatedValue.Sub(allocatedValue, maxPayoutCost) + + if allocatedValue.Cmp(common.Big0) < 0 { + return fmt.Errorf("negative payout") + } + + for _, refund := range refundConfig { + refundValue := common.PercentOf(allocatedValue, refund.Percent) + refundReceiver := refund.Address + rec, err := c.commitPayoutTx(refundValue, c.env.header.Coinbase, refundReceiver, core.SbundlePayoutMaxCostInt, key, chData) + if err != nil { + return err + } + if rec.Status != types.ReceiptStatusSuccessful { + return fmt.Errorf("refund tx failed") + } + log.Trace("Committed kickback", "payout", ethIntToFloat(allocatedValue), "receiver", refundReceiver) + } + } + coinbaseDelta.Set(c.env.state.GetBalance(c.env.header.Coinbase)) + coinbaseDelta.Sub(coinbaseDelta, coinbaseBefore) + totalProfit.Add(totalProfit, coinbaseDelta) + + if totalProfit.Cmp(common.Big0) < 0 { + return fmt.Errorf("negative profit") + } + return nil +} + +// discard reverts all changes to the environment - every commit operation must be followed by a discard or apply operation +func (c *envChanges) discard() error { + return c.env.state.MultiTxSnapshotRevert() +} + +// rollback reverts all changes to the environment - whereas apply and discard update the state, rollback only updates the environment +// the intended use is to call rollback after a commit operation has failed +func (c *envChanges) rollback( + gasUsedBefore uint64, gasPoolBefore *core.GasPool, profitBefore *big.Int, + txsBefore []*types.Transaction, receiptsBefore []*types.Receipt) { + c.usedGas = gasUsedBefore + c.gasPool = gasPoolBefore + c.txs = txsBefore + c.receipts = receiptsBefore + c.profit.Set(profitBefore) +} + +func (c *envChanges) apply() error { + if err := c.env.state.MultiTxSnapshotCommit(); err != nil { + return err + } + + c.env.gasPool.SetGas(c.gasPool.Gas()) + c.env.header.GasUsed = c.usedGas + c.env.profit.Set(c.profit) + c.env.tcount += len(c.txs) + c.env.txs = append(c.env.txs, c.txs...) + c.env.receipts = append(c.env.receipts, c.receipts...) + return nil +} diff --git a/miner/environment_diff.go b/miner/environment_diff.go new file mode 100644 index 0000000000..064ab60131 --- /dev/null +++ b/miner/environment_diff.go @@ -0,0 +1,418 @@ +package miner + +import ( + "crypto/ecdsa" + "errors" + "fmt" + "math/big" + "sync/atomic" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/log" +) + +// environmentDiff is a helper struct used to apply transactions to a block using a copy of the state at that block +type environmentDiff struct { + baseEnvironment *environment + header *types.Header + gasPool *core.GasPool // available gas used to pack transactions + state *state.StateDB // apply state changes here + newProfit *big.Int + newTxs []*types.Transaction + newReceipts []*types.Receipt +} + +func newEnvironmentDiff(env *environment) *environmentDiff { + gasPool := new(core.GasPool).AddGas(env.gasPool.Gas()) + return &environmentDiff{ + baseEnvironment: env, + header: types.CopyHeader(env.header), + gasPool: gasPool, + state: env.state.Copy(), + newProfit: new(big.Int), + } +} + +func (envDiff *environmentDiff) copy() *environmentDiff { + gasPool := new(core.GasPool).AddGas(envDiff.gasPool.Gas()) + + return &environmentDiff{ + baseEnvironment: envDiff.baseEnvironment.copy(), + header: types.CopyHeader(envDiff.header), + gasPool: gasPool, + state: envDiff.state.Copy(), + newProfit: new(big.Int).Set(envDiff.newProfit), + newTxs: envDiff.newTxs[:], + newReceipts: envDiff.newReceipts[:], + } +} + +func (envDiff *environmentDiff) applyToBaseEnv() { + env := envDiff.baseEnvironment + env.gasPool = new(core.GasPool).AddGas(envDiff.gasPool.Gas()) + env.header = envDiff.header + env.state.StopPrefetcher() + env.state = envDiff.state + env.profit.Add(env.profit, envDiff.newProfit) + env.tcount += len(envDiff.newTxs) + env.txs = append(env.txs, envDiff.newTxs...) + env.receipts = append(env.receipts, envDiff.newReceipts...) +} + +// commit tx to envDiff +func (envDiff *environmentDiff) commitTx(tx *types.Transaction, chData chainData) (*types.Receipt, int, error) { + header := envDiff.header + coinbase := &envDiff.baseEnvironment.coinbase + signer := envDiff.baseEnvironment.signer + + gasPrice, err := tx.EffectiveGasTip(header.BaseFee) + if err != nil { + return nil, shiftTx, err + } + + envDiff.state.SetTxContext(tx.Hash(), envDiff.baseEnvironment.tcount+len(envDiff.newTxs)) + + receipt, newState, err := applyTransactionWithBlacklist(signer, chData.chainConfig, chData.chain, coinbase, + envDiff.gasPool, envDiff.state, header, tx, &header.GasUsed, *chData.chain.GetVMConfig(), chData.blacklist) + + envDiff.state = newState + if err != nil { + switch { + case errors.Is(err, core.ErrGasLimitReached): + // Pop the current out-of-gas transaction without shifting in the next from the account + from, _ := types.Sender(signer, tx) + log.Trace("Gas limit exceeded for current block", "sender", from) + return receipt, popTx, err + + case errors.Is(err, core.ErrNonceTooLow): + // New head notification data race between the transaction pool and miner, shift + from, _ := types.Sender(signer, tx) + log.Trace("Skipping transaction with low nonce", "sender", from, "nonce", tx.Nonce()) + return receipt, shiftTx, err + + case errors.Is(err, core.ErrNonceTooHigh): + // Reorg notification data race between the transaction pool and miner, skip account = + from, _ := types.Sender(signer, tx) + log.Trace("Skipping account with hight nonce", "sender", from, "nonce", tx.Nonce()) + return receipt, popTx, err + + case errors.Is(err, core.ErrTxTypeNotSupported): + // Pop the unsupported transaction without shifting in the next from the account + from, _ := types.Sender(signer, tx) + log.Trace("Skipping unsupported transaction type", "sender", from, "type", tx.Type()) + return receipt, popTx, err + + default: + // Strange error, discard the transaction and get the next in line (note, the + // nonce-too-high clause will prevent us from executing in vain). + log.Trace("Transaction failed, account skipped", "hash", tx.Hash(), "err", err) + return receipt, shiftTx, err + } + } + + envDiff.newProfit = envDiff.newProfit.Add(envDiff.newProfit, gasPrice.Mul(gasPrice, big.NewInt(int64(receipt.GasUsed)))) + envDiff.newTxs = append(envDiff.newTxs, tx) + envDiff.newReceipts = append(envDiff.newReceipts, receipt) + + return receipt, shiftTx, nil +} + +// Commit Bundle to env diff +func (envDiff *environmentDiff) commitBundle(bundle *types.SimulatedBundle, chData chainData, interrupt *atomic.Int32, algoConf algorithmConfig) error { + coinbase := envDiff.baseEnvironment.coinbase + tmpEnvDiff := envDiff.copy() + + coinbaseBalanceBefore := tmpEnvDiff.state.GetBalance(coinbase) + + profitBefore := new(big.Int).Set(tmpEnvDiff.newProfit) + var gasUsed uint64 + + for _, tx := range bundle.OriginalBundle.Txs { + txHash := tx.Hash() + if tmpEnvDiff.header.BaseFee != nil && tx.Type() == types.DynamicFeeTxType { + // Sanity check for extremely large numbers + if tx.GasFeeCap().BitLen() > 256 { + return core.ErrFeeCapVeryHigh + } + if tx.GasTipCap().BitLen() > 256 { + return core.ErrTipVeryHigh + } + // Ensure gasFeeCap is greater than or equal to gasTipCap. + if tx.GasFeeCapIntCmp(tx.GasTipCap()) < 0 { + return core.ErrTipAboveFeeCap + } + } + + if tx.Value().Sign() == -1 { + return core.ErrNegativeValue + } + + _, err := tx.EffectiveGasTip(envDiff.header.BaseFee) + if err != nil { + return err + } + + _, err = types.Sender(envDiff.baseEnvironment.signer, tx) + if err != nil { + return err + } + + if checkInterrupt(interrupt) { + return errInterrupt + } + + receipt, _, err := tmpEnvDiff.commitTx(tx, chData) + + if err != nil { + isRevertibleTx := bundle.OriginalBundle.RevertingHash(txHash) + // if drop enabled, and revertible tx has error on commit, we skip the transaction and continue with next one + if algoConf.DropRevertibleTxOnErr && isRevertibleTx { + log.Trace("Found error on commit for revertible tx, but discard on err is enabled so skipping.", + "tx", txHash, "err", err) + continue + } + log.Trace("Bundle tx error", "bundle", bundle.OriginalBundle.Hash, "tx", txHash, "err", err) + return err + } + + if receipt != nil { + if receipt.Status == types.ReceiptStatusFailed && !bundle.OriginalBundle.RevertingHash(txHash) { + // if transaction reverted and isn't specified as reverting hash, return error + log.Trace("Bundle tx failed", "bundle", bundle.OriginalBundle.Hash, "tx", txHash, "err", err) + return errors.New("bundle tx revert") + } + } else { + // NOTE: The expectation is that a receipt is only nil if an error occurred. + // If there is no error but receipt is nil, there is likely a programming error. + return errors.New("invalid receipt when no error occurred") + } + + gasUsed += receipt.GasUsed + } + coinbaseBalanceAfter := tmpEnvDiff.state.GetBalance(coinbase) + coinbaseBalanceDelta := new(big.Int).Sub(coinbaseBalanceAfter, coinbaseBalanceBefore) + tmpEnvDiff.newProfit.Add(profitBefore, coinbaseBalanceDelta) + + if bundle.MevGasPrice == nil { + return ErrMevGasPriceNotSet + } + + var ( + bundleProfit = coinbaseBalanceDelta + // EGP = Effective Gas Price (Profit / GasUsed) + simulatedEGP = new(big.Int).Set(bundle.MevGasPrice) + actualEGP *big.Int + tolerablePriceDifferencePercent = 1 + + simulatedBundleProfit = new(big.Int).Set(bundle.TotalEth) + actualBundleProfit = new(big.Int).Set(bundleProfit) + ) + + if gasUsed == 0 { + return errors.New("bundle gas used is 0") + } else { + actualEGP = new(big.Int).Div(bundleProfit, big.NewInt(int64(gasUsed))) + } + + err := ValidateGasPriceAndProfit(algoConf, + actualEGP, simulatedEGP, tolerablePriceDifferencePercent, + actualBundleProfit, simulatedBundleProfit, + ) + if err != nil { + return err + } + + *envDiff = *tmpEnvDiff + return nil +} + +func (envDiff *environmentDiff) commitPayoutTx(amount *big.Int, sender, receiver common.Address, gas uint64, prv *ecdsa.PrivateKey, chData chainData) (*types.Receipt, error) { + return commitPayoutTx(PayoutTransactionParams{ + Amount: amount, + BaseFee: envDiff.header.BaseFee, + ChainData: chData, + Gas: gas, + CommitFn: envDiff.commitTx, + Receiver: receiver, + Sender: sender, + SenderBalance: envDiff.state.GetBalance(sender), + SenderNonce: envDiff.state.GetNonce(sender), + Signer: envDiff.baseEnvironment.signer, + PrivateKey: prv, + }) +} + +func (envDiff *environmentDiff) commitSBundle(b *types.SimSBundle, chData chainData, interrupt *atomic.Int32, key *ecdsa.PrivateKey, algoConf algorithmConfig) error { + // TODO: Suggestion for future improvement: instead of checking if key is nil, panic. + // Discussed with @Ruteri, see PR#90 for details: https://github.com/flashbots/builder/pull/90#discussion_r1285567550 + if key == nil { + return errNoPrivateKey + } + + tmpEnvDiff := envDiff.copy() + + coinbaseBefore := tmpEnvDiff.state.GetBalance(tmpEnvDiff.header.Coinbase) + gasBefore := tmpEnvDiff.gasPool.Gas() + + if err := tmpEnvDiff.commitSBundleInner(b.Bundle, chData, interrupt, key, algoConf); err != nil { + return err + } + + coinbaseAfter := tmpEnvDiff.state.GetBalance(tmpEnvDiff.header.Coinbase) + gasAfter := tmpEnvDiff.gasPool.Gas() + + coinbaseDelta := new(big.Int).Sub(coinbaseAfter, coinbaseBefore) + gasDelta := new(big.Int).SetUint64(gasBefore - gasAfter) + + if coinbaseDelta.Cmp(common.Big0) < 0 { + return errors.New("coinbase balance decreased") + } + + gotEGP := new(big.Int).Div(coinbaseDelta, gasDelta) + simEGP := new(big.Int).Set(b.MevGasPrice) + + // allow > 1% difference + actualEGP := new(big.Int).Mul(gotEGP, big.NewInt(101)) + simulatedEGP := new(big.Int).Mul(simEGP, common.Big100) + + if simulatedEGP.Cmp(actualEGP) > 0 { + return &lowProfitError{ + ExpectedEffectiveGasPrice: simEGP, + ActualEffectiveGasPrice: gotEGP, + } + } + + if algoConf.EnforceProfit { + // if profit is enforced between simulation and actual commit, only allow >-1% divergence + simulatedSbundleProfit := new(big.Int).Set(b.Profit) + actualSbundleProfit := new(big.Int).Set(coinbaseDelta) + + // We want to make simulated profit smaller to allow for some leeway in cases where the actual profit is + // lower due to transaction ordering + simulatedProfitMultiple := common.PercentOf(simulatedSbundleProfit, algoConf.ProfitThresholdPercent) + actualProfitMultiple := new(big.Int).Mul(actualSbundleProfit, common.Big100) + + if simulatedProfitMultiple.Cmp(actualProfitMultiple) > 0 { + log.Trace("Lower sbundle profit found after inclusion", "sbundle", b.Bundle.Hash()) + return &lowProfitError{ + ExpectedProfit: simulatedSbundleProfit, + ActualProfit: actualSbundleProfit, + } + } + } + + *envDiff = *tmpEnvDiff + return nil +} + +func (envDiff *environmentDiff) commitSBundleInner(b *types.SBundle, chData chainData, interrupt *atomic.Int32, key *ecdsa.PrivateKey, algoConf algorithmConfig) error { + // check inclusion + minBlock := b.Inclusion.BlockNumber + maxBlock := b.Inclusion.MaxBlockNumber + if current := envDiff.header.Number.Uint64(); current < minBlock || current > maxBlock { + return fmt.Errorf("bundle inclusion block number out of range: %d <= %d <= %d", minBlock, current, maxBlock) + } + + // extract constraints into convenient format + refundIdx := make([]bool, len(b.Body)) + refundPercents := make([]int, len(b.Body)) + for _, el := range b.Validity.Refund { + refundIdx[el.BodyIdx] = true + refundPercents[el.BodyIdx] = el.Percent + } + + var ( + totalProfit *big.Int = new(big.Int) + refundableProfit *big.Int = new(big.Int) + ) + + var ( + coinbaseDelta = new(big.Int) + coinbaseBefore *big.Int + ) + // insert body and check it + for i, el := range b.Body { + coinbaseDelta.Set(common.Big0) + coinbaseBefore = envDiff.state.GetBalance(envDiff.header.Coinbase) + + if el.Tx != nil { + receipt, _, err := envDiff.commitTx(el.Tx, chData) + if err != nil { + // if drop enabled, and revertible tx has error on commit, + // we skip the transaction and continue with next one + if algoConf.DropRevertibleTxOnErr && el.CanRevert { + log.Trace("Found error on commit for revertible tx, but discard on err is enabled so skipping.", + "tx", el.Tx.Hash(), "err", err) + continue + } + return err + } + if receipt.Status != types.ReceiptStatusSuccessful && !el.CanRevert { + return errors.New("tx failed") + } + } else if el.Bundle != nil { + err := envDiff.commitSBundleInner(el.Bundle, chData, interrupt, key, algoConf) + if err != nil { + return err + } + } else { + return errors.New("invalid body element") + } + + coinbaseDelta.Set(envDiff.state.GetBalance(envDiff.header.Coinbase)) + coinbaseDelta.Sub(coinbaseDelta, coinbaseBefore) + + totalProfit.Add(totalProfit, coinbaseDelta) + if !refundIdx[i] { + refundableProfit.Add(refundableProfit, coinbaseDelta) + } + } + + // enforce constraints + coinbaseDelta.Set(common.Big0) + coinbaseBefore = envDiff.state.GetBalance(envDiff.header.Coinbase) + for i, el := range refundPercents { + if !refundIdx[i] { + continue + } + refundConfig, err := types.GetRefundConfig(&b.Body[i], envDiff.baseEnvironment.signer) + if err != nil { + return err + } + + maxPayoutCost := new(big.Int).Set(core.SbundlePayoutMaxCost) + maxPayoutCost.Mul(maxPayoutCost, big.NewInt(int64(len(refundConfig)))) + maxPayoutCost.Mul(maxPayoutCost, envDiff.header.BaseFee) + + allocatedValue := common.PercentOf(refundableProfit, el) + allocatedValue.Sub(allocatedValue, maxPayoutCost) + + if allocatedValue.Cmp(common.Big0) < 0 { + return fmt.Errorf("negative payout") + } + + for _, refund := range refundConfig { + refundValue := common.PercentOf(allocatedValue, refund.Percent) + refundReceiver := refund.Address + rec, err := envDiff.commitPayoutTx(refundValue, envDiff.header.Coinbase, refundReceiver, core.SbundlePayoutMaxCostInt, key, chData) + if err != nil { + return err + } + if rec.Status != types.ReceiptStatusSuccessful { + return fmt.Errorf("refund tx failed") + } + log.Trace("Committed kickback", "payout", ethIntToFloat(allocatedValue), "receiver", refundReceiver) + } + } + coinbaseDelta.Set(envDiff.state.GetBalance(envDiff.header.Coinbase)) + coinbaseDelta.Sub(coinbaseDelta, coinbaseBefore) + totalProfit.Add(totalProfit, coinbaseDelta) + + if totalProfit.Cmp(common.Big0) < 0 { + return fmt.Errorf("negative profit") + } + return nil +} diff --git a/miner/miner.go b/miner/miner.go index b55ba98481..8d94d19536 100644 --- a/miner/miner.go +++ b/miner/miner.go @@ -18,8 +18,13 @@ package miner import ( + "crypto/ecdsa" + "errors" "fmt" + "github.com/ethereum/go-ethereum/crypto" "math/big" + "os" + "strings" "sync" "time" @@ -44,20 +49,69 @@ type Backend interface { PeerCount() int } +type AlgoType int + +const ( + ALGO_MEV_GETH AlgoType = iota + ALGO_GREEDY + ALGO_GREEDY_BUCKETS + ALGO_GREEDY_MULTISNAP + ALGO_GREEDY_BUCKETS_MULTISNAP +) + +func (a AlgoType) String() string { + switch a { + case ALGO_GREEDY: + return "greedy" + case ALGO_GREEDY_MULTISNAP: + return "greedy-multi-snap" + case ALGO_MEV_GETH: + return "mev-geth" + case ALGO_GREEDY_BUCKETS: + return "greedy-buckets" + case ALGO_GREEDY_BUCKETS_MULTISNAP: + return "greedy-buckets-multi-snap" + default: + return "unsupported" + } +} + +func AlgoTypeFlagToEnum(algoString string) (AlgoType, error) { + switch strings.ToLower(algoString) { + case ALGO_MEV_GETH.String(): + return ALGO_MEV_GETH, nil + case ALGO_GREEDY_BUCKETS.String(): + return ALGO_GREEDY_BUCKETS, nil + case ALGO_GREEDY.String(): + return ALGO_GREEDY, nil + case ALGO_GREEDY_MULTISNAP.String(): + return ALGO_GREEDY_MULTISNAP, nil + case ALGO_GREEDY_BUCKETS_MULTISNAP.String(): + return ALGO_GREEDY_BUCKETS_MULTISNAP, nil + default: + return ALGO_MEV_GETH, errors.New("algo not recognized") + } +} + // Config is the configuration parameters of mining. type Config struct { - Etherbase common.Address `toml:",omitempty"` // Public address for block mining rewards - Notify []string `toml:",omitempty"` // HTTP URL list to be notified of new work packages (only useful in ethash). - NotifyFull bool `toml:",omitempty"` // Notify with pending block headers instead of work packages - ExtraData hexutil.Bytes `toml:",omitempty"` // Block extra data set by the miner - GasFloor uint64 // Target gas floor for mined blocks. - GasCeil uint64 // Target gas ceiling for mined blocks. - GasPrice *big.Int // Minimum gas price for mining a transaction - Recommit time.Duration // The time interval for miner to re-create mining work. - Noverify bool // Disable remote mining solution verification(only useful in ethash). - CommitInterruptFlag bool // Interrupt commit when time is up ( default = true) - - NewPayloadTimeout time.Duration // The maximum time allowance for creating a new payload + Etherbase common.Address `toml:",omitempty"` // Public address for block mining rewards + Notify []string `toml:",omitempty"` // HTTP URL list to be notified of new work packages (only useful in ethash). + NotifyFull bool `toml:",omitempty"` // Notify with pending block headers instead of work packages + ExtraData hexutil.Bytes `toml:",omitempty"` // Block extra data set by the miner + GasFloor uint64 // Target gas floor for mined blocks. + GasCeil uint64 // Target gas ceiling for mined blocks. + GasPrice *big.Int // Minimum gas price for mining a transaction + AlgoType AlgoType // Algorithm to use for block building + Recommit time.Duration // The time interval for miner to re-create mining work. + Noverify bool // Disable remote mining solution verification(only useful in ethash). + CommitInterruptFlag bool // Interrupt commit when time is up ( default = true) + BuilderTxSigningKey *ecdsa.PrivateKey `toml:",omitempty"` // Signing key of builder coinbase to make transaction to validator + MaxMergedBundles int + Blocklist []common.Address `toml:",omitempty"` + NewPayloadTimeout time.Duration // The maximum time allowance for creating a new payload + PriceCutoffPercent int // Effective gas price cutoff % used for bucketing transactions by price (only useful in greedy-buckets AlgoType) + DiscardRevertibleTxOnErr bool // When enabled, if bundle revertible transaction has error on commit, builder will discard the transaction } // DefaultConfig contains default settings for miner. @@ -69,8 +123,9 @@ var DefaultConfig = Config{ // consensus-layer usually will wait a half slot of time(6s) // for payload generation. It should be enough for Geth to // run 3 rounds. - Recommit: 2 * time.Second, - NewPayloadTimeout: 2 * time.Second, + Recommit: 2 * time.Second, + NewPayloadTimeout: 2 * time.Second, + PriceCutoffPercent: defaultPriceCutoffPercent, } // Miner creates blocks and searches for proof-of-work values. @@ -82,20 +137,29 @@ type Miner struct { exitCh chan struct{} startCh chan struct{} stopCh chan chan struct{} - worker *worker + worker *multiWorker wg sync.WaitGroup } func New(eth Backend, config *Config, chainConfig *params.ChainConfig, mux *event.TypeMux, engine consensus.Engine, isLocalBlock func(header *types.Header) bool) *Miner { + if config.BuilderTxSigningKey == nil { + key := os.Getenv("BUILDER_TX_SIGNING_KEY") + if key, err := crypto.HexToECDSA(strings.TrimPrefix(key, "0x")); err != nil { + log.Error("Error parsing builder signing key from env", "err", err) + } else { + config.BuilderTxSigningKey = key + } + } + miner := &Miner{ mux: mux, eth: eth, engine: engine, exitCh: make(chan struct{}), - stopCh: make(chan chan struct{}), startCh: make(chan struct{}), - worker: newWorker(config, chainConfig, engine, eth, mux, isLocalBlock, true), + stopCh: make(chan chan struct{}), + worker: newMultiWorker(config, chainConfig, engine, eth, mux, isLocalBlock, true), } miner.wg.Add(1) @@ -105,7 +169,8 @@ func New(eth Backend, config *Config, chainConfig *params.ChainConfig, mux *even } func (miner *Miner) GetWorker() *worker { - return miner.worker + // TODO [pnowosie] where is it used? + return miner.worker.workers[0] //?? } // update keeps track of the downloader events. Please be aware that this is a one shot type of update loop. @@ -195,7 +260,7 @@ func (miner *Miner) Close() { } func (miner *Miner) Mining() bool { - return miner.worker.IsRunning() + return miner.worker.isRunning() } func (miner *Miner) Hashrate() uint64 { @@ -223,7 +288,7 @@ func (miner *Miner) SetRecommitInterval(interval time.Duration) { // Pending returns the currently pending block and associated state. func (miner *Miner) Pending() (*types.Block, *state.StateDB) { - return miner.worker.pending() + return miner.worker.regularWorker.pending() } // PendingBlock returns the currently pending block. @@ -232,7 +297,7 @@ func (miner *Miner) Pending() (*types.Block, *state.StateDB) { // simultaneously, please use Pending(), as the pending state can // change between multiple method calls func (miner *Miner) PendingBlock() *types.Block { - return miner.worker.pendingBlock() + return miner.worker.regularWorker.pendingBlock() } // PendingBlockAndReceipts returns the currently pending block and corresponding receipts. @@ -270,9 +335,12 @@ func (miner *Miner) DisablePreseal() { // SubscribePendingLogs starts delivering logs from pending transactions // to the given channel. func (miner *Miner) SubscribePendingLogs(ch chan<- []*types.Log) event.Subscription { - return miner.worker.pendingLogsFeed.Subscribe(ch) + return miner.worker.regularWorker.pendingLogsFeed.Subscribe(ch) } +// Accepts the block, time at which orders were taken, bundles which were used to build the block and all bundles that were considered for the block +type BlockHookFn = func(*types.Block, *big.Int, time.Time, []types.SimulatedBundle, []types.SimulatedBundle, []types.UsedSBundle) + // BuildPayload builds the payload according to the provided parameters. func (miner *Miner) BuildPayload(args *BuildPayloadArgs) (*Payload, error) { return miner.worker.buildPayload(args) diff --git a/miner/multi_worker.go b/miner/multi_worker.go new file mode 100644 index 0000000000..3abc9516be --- /dev/null +++ b/miner/multi_worker.go @@ -0,0 +1,209 @@ +package miner + +import ( + "errors" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/consensus" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/params" +) + +type multiWorker struct { + workers []*worker + regularWorker *worker +} + +func (w *multiWorker) stop() { + for _, worker := range w.workers { + worker.stop() + } +} + +func (w *multiWorker) start() { + for _, worker := range w.workers { + worker.start() + } +} + +func (w *multiWorker) close() { + for _, worker := range w.workers { + worker.close() + } +} + +func (w *multiWorker) isRunning() bool { + for _, worker := range w.workers { + if worker.IsRunning() { + return true + } + } + return false +} + +// pendingBlockAndReceipts returns pending block and corresponding receipts from the `regularWorker` +func (w *multiWorker) pendingBlockAndReceipts() (*types.Block, types.Receipts) { + // return a snapshot to avoid contention on currentMu mutex + return w.regularWorker.pendingBlockAndReceipts() +} + +func (w *multiWorker) setGasCeil(ceil uint64) { + for _, worker := range w.workers { + worker.setGasCeil(ceil) + } +} + +func (w *multiWorker) setExtra(extra []byte) { + for _, worker := range w.workers { + worker.setExtra(extra) + } +} + +func (w *multiWorker) setRecommitInterval(interval time.Duration) { + for _, worker := range w.workers { + worker.setRecommitInterval(interval) + } +} + +func (w *multiWorker) setEtherbase(addr common.Address) { + for _, worker := range w.workers { + worker.setEtherbase(addr) + } +} + +func (w *multiWorker) enablePreseal() { + for _, worker := range w.workers { + worker.enablePreseal() + } +} + +func (w *multiWorker) disablePreseal() { + for _, worker := range w.workers { + worker.disablePreseal() + } +} + +func (w *multiWorker) buildPayload(args *BuildPayloadArgs) (*Payload, error) { + // Build the initial version with no transaction included. It should be fast + // enough to run. The empty payload can at least make sure there is something + // to deliver for not missing slot. + var empty *types.Block + for _, worker := range w.workers { + var err error + empty, _, err = worker.getSealingBlock(args.Parent, args.Timestamp, args.FeeRecipient, args.GasLimit, args.Random, args.Withdrawals, true, nil) + if err != nil { + log.Error("could not start async block construction", "isFlashbotsWorker", worker.flashbots.isFlashbots, "#bundles", worker.flashbots.maxMergedBundles) + continue + } + break + } + + if empty == nil { + return nil, errors.New("no worker could build an empty block") + } + + // Construct a payload object for return. + payload := newPayload(empty, args.Id()) + + if len(w.workers) == 0 { + return payload, nil + } + + // Keep separate payloads for each worker so that ResolveFull actually resolves the best of all workers + workerPayloads := []*Payload{} + + for _, w := range w.workers { + workerPayload := newPayload(empty, args.Id()) + workerPayloads = append(workerPayloads, workerPayload) + + go func(w *worker) { + // Update routine done elsewhere! + start := time.Now() + block, fees, err := w.getSealingBlock(args.Parent, args.Timestamp, args.FeeRecipient, args.GasLimit, args.Random, args.Withdrawals, false, args.BlockHook) + if err == nil { + workerPayload.update(block, fees, time.Since(start)) + } else { + log.Error("Error while sealing block", "err", err) + workerPayload.Cancel() + } + }(w) + } + + go payload.resolveBestFullPayload(workerPayloads) + + return payload, nil +} + +func newMultiWorker(config *Config, chainConfig *params.ChainConfig, engine consensus.Engine, eth Backend, mux *event.TypeMux, isLocalBlock func(header *types.Header) bool, init bool) *multiWorker { + switch config.AlgoType { + case ALGO_MEV_GETH: + return newMultiWorkerMevGeth(config, chainConfig, engine, eth, mux, isLocalBlock, init) + case ALGO_GREEDY, ALGO_GREEDY_BUCKETS, ALGO_GREEDY_MULTISNAP, ALGO_GREEDY_BUCKETS_MULTISNAP: + return newMultiWorkerGreedy(config, chainConfig, engine, eth, mux, isLocalBlock, init) + default: + panic("unsupported builder algorithm found") + } +} + +func newMultiWorkerGreedy(config *Config, chainConfig *params.ChainConfig, engine consensus.Engine, eth Backend, mux *event.TypeMux, isLocalBlock func(header *types.Header) bool, init bool) *multiWorker { + queue := make(chan *task) + + greedyWorker := newWorker(config, chainConfig, engine, eth, mux, isLocalBlock, init, &flashbotsData{ + isFlashbots: true, + queue: queue, + algoType: config.AlgoType, + maxMergedBundles: config.MaxMergedBundles, + bundleCache: NewBundleCache(), + }) + + log.Info("creating new greedy worker") + return &multiWorker{ + regularWorker: greedyWorker, + workers: []*worker{greedyWorker}, + } +} + +func newMultiWorkerMevGeth(config *Config, chainConfig *params.ChainConfig, engine consensus.Engine, eth Backend, mux *event.TypeMux, isLocalBlock func(header *types.Header) bool, init bool) *multiWorker { + queue := make(chan *task) + + bundleCache := NewBundleCache() + + regularWorker := newWorker(config, chainConfig, engine, eth, mux, isLocalBlock, init, &flashbotsData{ + isFlashbots: false, + queue: queue, + algoType: ALGO_MEV_GETH, + maxMergedBundles: config.MaxMergedBundles, + bundleCache: bundleCache, + }) + + workers := []*worker{regularWorker} + if config.AlgoType == ALGO_MEV_GETH { + for i := 1; i <= config.MaxMergedBundles; i++ { + workers = append(workers, + newWorker(config, chainConfig, engine, eth, mux, isLocalBlock, init, &flashbotsData{ + isFlashbots: true, + queue: queue, + algoType: ALGO_MEV_GETH, + maxMergedBundles: i, + bundleCache: bundleCache, + })) + } + } + + log.Info("creating multi worker", "config.MaxMergedBundles", config.MaxMergedBundles, "workers", len(workers)) + return &multiWorker{ + regularWorker: regularWorker, + workers: workers, + } +} + +type flashbotsData struct { + isFlashbots bool + queue chan *task + maxMergedBundles int + algoType AlgoType + bundleCache *BundleCache +} diff --git a/miner/payload_building.go b/miner/payload_building.go index 0009a9f487..2ecd3925dd 100644 --- a/miner/payload_building.go +++ b/miner/payload_building.go @@ -35,11 +35,13 @@ import ( // Check engine-api specification for more details. // https://github.com/ethereum/execution-apis/blob/main/src/engine/specification.md#payloadattributesv1 type BuildPayloadArgs struct { - Parent common.Hash // The parent block to build payload on top - Timestamp uint64 // The provided timestamp of generated payload - FeeRecipient common.Address // The provided recipient address for collecting transaction fee + Parent common.Hash // The parent block to build payload on top + Timestamp uint64 // The provided timestamp of generated payload + FeeRecipient common.Address // The provided recipient address for collecting transaction fee + GasLimit uint64 Random common.Hash // The provided randomness value Withdrawals types.Withdrawals // The provided withdrawals + BlockHook BlockHookFn } // Id computes an 8-byte identifier by hashing the components of the payload arguments. @@ -48,6 +50,7 @@ func (args *BuildPayloadArgs) Id() engine.PayloadID { hasher := sha256.New() hasher.Write(args.Parent[:]) _ = binary.Write(hasher, binary.BigEndian, args.Timestamp) + _ = binary.Write(hasher, binary.BigEndian, args.GasLimit) hasher.Write(args.Random[:]) hasher.Write(args.FeeRecipient[:]) _ = rlp.Encode(hasher, args.Withdrawals) @@ -113,6 +116,62 @@ func (payload *Payload) update(block *types.Block, fees *big.Int, elapsed time.D payload.cond.Broadcast() // fire signal for notifying full block } +func (payload *Payload) resolveBestFullPayload(payloads []*Payload) { + payload.lock.Lock() + defer payload.lock.Unlock() + + log.Trace("resolving best payload") + for _, p := range payloads { + p.lock.Lock() + + if p.full == nil { + select { + case <-p.stop: + p.lock.Unlock() + continue + default: + p.cond.Wait() + } + + if p.full == nil { + p.lock.Unlock() + continue + } + } + if payload.full == nil || payload.fullFees.Cmp(p.fullFees) < 0 { + log.Trace("best payload updated", "id", p.id, "blockHash", p.full.Hash()) + payload.full = p.full + payload.fullFees = p.fullFees + } + p.lock.Unlock() + } + + // Since we are not expecting any updates, close the payload already + select { + case <-payload.stop: + default: + close(payload.stop) + } + + payload.cond.Broadcast() // fire signal for notifying full block + + if payload.full != nil { + log.Trace("best payload resolved", "id", payload.id, "blockHash", payload.full.Hash()) + } else { + log.Trace("no payload resolved", "id", payload.id) + } +} + +func (payload *Payload) Cancel() { + select { + case <-payload.stop: + default: + close(payload.stop) + } + + payload.cond.Broadcast() +} + // Resolve returns the latest built payload and also terminates the background // thread for updating payload. It's safe to be called multiple times. func (payload *Payload) Resolve() *engine.ExecutionPayloadEnvelope { @@ -156,6 +215,10 @@ func (payload *Payload) ResolveFull() *engine.ExecutionPayloadEnvelope { payload.cond.Wait() } + if payload.full == nil { + return nil + } + return engine.BlockToExecutableData(payload.full, payload.fullFees) } @@ -164,7 +227,7 @@ func (w *worker) buildPayload(args *BuildPayloadArgs) (*Payload, error) { // Build the initial version with no transaction included. It should be fast // enough to run. The empty payload can at least make sure there is something // to deliver for not missing slot. - empty, _, err := w.getSealingBlock(args.Parent, args.Timestamp, args.FeeRecipient, args.Random, args.Withdrawals, true) + empty, _, err := w.getSealingBlock(args.Parent, args.Timestamp, args.FeeRecipient, args.GasLimit, args.Random, args.Withdrawals, true, args.BlockHook) if err != nil { return nil, err } @@ -188,7 +251,7 @@ func (w *worker) buildPayload(args *BuildPayloadArgs) (*Payload, error) { select { case <-timer.C: start := time.Now() - block, fees, err := w.getSealingBlock(args.Parent, args.Timestamp, args.FeeRecipient, args.Random, args.Withdrawals, false) + block, fees, err := w.getSealingBlock(args.Parent, args.Timestamp, args.FeeRecipient, args.GasLimit, args.Random, args.Withdrawals, false, args.BlockHook) if err == nil { payload.update(block, fees, time.Since(start)) diff --git a/miner/test_backend.go b/miner/test_backend.gone similarity index 99% rename from miner/test_backend.go rename to miner/test_backend.gone index bb63f1b562..e3ed4f3d49 100644 --- a/miner/test_backend.go +++ b/miner/test_backend.gone @@ -441,7 +441,7 @@ func (w *worker) mainLoopWithDelay(ctx context.Context, delay uint, opcodeDelay txs[acc] = append(txs[acc], tx) } - txset := types.NewTransactionsByPriceAndNonce(w.current.signer, txs, cmath.FromBig(w.current.header.BaseFee)) + txset := types.NewTransactionsByPriceAndNonce(w.current.signer, txs, nil, nil, cmath.FromBig(w.current.header.BaseFee)) tcount := w.current.tcount w.commitTransactions(w.current, txset, nil, context.Background()) @@ -675,7 +675,7 @@ func (w *worker) fillTransactionsWithDelay(ctx context.Context, interrupt *int32 var txs *types.TransactionsByPriceAndNonce tracing.Exec(ctx, "", "worker.LocalTransactionsByPriceAndNonce", func(ctx context.Context, span trace.Span) { - txs = types.NewTransactionsByPriceAndNonce(env.signer, localTxs, cmath.FromBig(env.header.BaseFee)) + txs = types.NewTransactionsByPriceAndNonce(env.signer, localTxs, nil, nil, cmath.FromBig(env.header.BaseFee)) tracing.SetAttributes( span, @@ -698,7 +698,7 @@ func (w *worker) fillTransactionsWithDelay(ctx context.Context, interrupt *int32 var txs *types.TransactionsByPriceAndNonce tracing.Exec(ctx, "", "worker.RemoteTransactionsByPriceAndNonce", func(ctx context.Context, span trace.Span) { - txs = types.NewTransactionsByPriceAndNonce(env.signer, remoteTxs, cmath.FromBig(env.header.BaseFee)) + txs = types.NewTransactionsByPriceAndNonce(env.signer, remoteTxs, nil, nil, cmath.FromBig(env.header.BaseFee)) tracing.SetAttributes( span, @@ -795,12 +795,13 @@ mainloop: break } // Retrieve the next transaction and abort if all done - tx := txs.Peek() - if tx == nil { + txmf := txs.Peek() + if txmf == nil { // nolint:goconst breakCause = "all transactions has been included" break } + tx := txmf.Tx() // Error may be ignored here. The error has already been checked // during transaction acceptance is the transaction pool. // diff --git a/miner/worker.go b/miner/worker.go index da27492847..ce2bcf8a49 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -21,11 +21,13 @@ import ( "context" "errors" "fmt" + "github.com/ethereum/go-ethereum/crypto" "math/big" "os" "runtime" "runtime/pprof" ptrace "runtime/trace" + "sort" "sync" "sync/atomic" "time" @@ -44,7 +46,6 @@ import ( "github.com/ethereum/go-ethereum/consensus/bor" "github.com/ethereum/go-ethereum/consensus/misc" "github.com/ethereum/go-ethereum/core" - "github.com/ethereum/go-ethereum/core/blockstm" "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/core/vm" @@ -52,13 +53,12 @@ import ( "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/metrics" "github.com/ethereum/go-ethereum/params" - "github.com/ethereum/go-ethereum/rlp" "github.com/ethereum/go-ethereum/trie" ) const ( // resultQueueSize is the size of channel listening to sealing result. - resultQueueSize = 10 + resultQueueSize = 20 // txChanSize is the size of channel listening to NewTxsEvent. // The number is referenced from the size of tx pool. @@ -97,8 +97,10 @@ const ( ) var ( + errCouldNotApplyTransaction = errors.New("could not apply transaction") errBlockInterruptedByNewHead = errors.New("new head arrived while building block") errBlockInterruptedByRecommit = errors.New("recommit interrupt while building block") + errBlocklistViolation = errors.New("blocklist violation") errBlockInterruptedByTimeout = errors.New("timeout while building block") // metrics gauge to track total and empty blocks sealed by a miner @@ -118,6 +120,7 @@ type environment struct { tcount int // tx count in cycle gasPool *core.GasPool // available gas used to pack transactions coinbase common.Address + profit *big.Int header *types.Header txs []*types.Transaction @@ -134,6 +137,7 @@ func (env *environment) copy() *environment { family: env.family.Clone(), tcount: env.tcount, coinbase: env.coinbase, + profit: new(big.Int).Set(env.profit), header: types.CopyHeader(env.header), receipts: copyReceipts(env.receipts), } @@ -184,6 +188,10 @@ type task struct { state *state.StateDB block *types.Block createdAt time.Time + + profit *big.Int + isFlashbots bool + worker int } const ( @@ -231,6 +239,7 @@ type worker struct { engine consensus.Engine eth Backend chain *core.BlockChain + blockList map[common.Address]struct{} // Feeds pendingLogsFeed event.Feed @@ -298,6 +307,8 @@ type worker struct { // External functions isLocalBlock func(header *types.Header) bool // Function used to determine whether the specified block is mined by local miner. + flashbots *flashbotsData + // Test hooks newTaskHook func(*task) // Method to call upon receiving a new sealing task. skipSealHook func(*task) bool // Method to decide whether skipping the sealing. @@ -310,19 +321,55 @@ type worker struct { } //nolint:staticcheck -func newWorker(config *Config, chainConfig *params.ChainConfig, engine consensus.Engine, eth Backend, mux *event.TypeMux, isLocalBlock func(header *types.Header) bool, init bool) *worker { +func newWorker(config *Config, chainConfig *params.ChainConfig, engine consensus.Engine, eth Backend, + mux *event.TypeMux, isLocalBlock func(header *types.Header) bool, init bool, flashbots *flashbotsData) *worker { + var builderCoinbase common.Address + if config.BuilderTxSigningKey == nil { + log.Error("Builder tx signing key is not set") + builderCoinbase = config.Etherbase + } else { + builderCoinbase = crypto.PubkeyToAddress(config.BuilderTxSigningKey.PublicKey) + } + + log.Info("new worker", "builderCoinbase", builderCoinbase.String()) + exitCh := make(chan struct{}) + taskCh := make(chan *task) + if flashbots.algoType == ALGO_MEV_GETH { + if flashbots.isFlashbots { + // publish to the flashbots queue + taskCh = flashbots.queue + } else { + // read from the flashbots queue + go func() { + for { + select { + case flashbotsTask := <-flashbots.queue: + select { + case taskCh <- flashbotsTask: + case <-exitCh: + return + } + case <-exitCh: + return + } + } + }() + } + } + worker := &worker{ - config: config, - chainConfig: chainConfig, - engine: engine, - eth: eth, - chain: eth.BlockChain(), - mux: mux, - isLocalBlock: isLocalBlock, - localUncles: make(map[common.Hash]*types.Block), - remoteUncles: make(map[common.Hash]*types.Block), - unconfirmed: newUnconfirmedBlocks(eth.BlockChain(), sealingLogAtDepth), - coinbase: config.Etherbase, + config: config, + chainConfig: chainConfig, + engine: engine, + eth: eth, + chain: eth.BlockChain(), + blockList: make(map[common.Address]struct{}), + mux: mux, + isLocalBlock: isLocalBlock, + localUncles: make(map[common.Hash]*types.Block), + remoteUncles: make(map[common.Hash]*types.Block), + unconfirmed: newUnconfirmedBlocks(eth.BlockChain(), sealingLogAtDepth), + //coinbase: config.Etherbase, extra: config.ExtraData, pendingTasks: make(map[common.Hash]*task), txsCh: make(chan core.NewTxsEvent, txChanSize), @@ -330,13 +377,15 @@ func newWorker(config *Config, chainConfig *params.ChainConfig, engine consensus chainSideCh: make(chan core.ChainSideEvent, chainSideChanSize), newWorkCh: make(chan *newWorkReq), getWorkCh: make(chan *getWorkReq), - taskCh: make(chan *task), + taskCh: taskCh, resultCh: make(chan *types.Block, resultQueueSize), startCh: make(chan struct{}, 1), - exitCh: make(chan struct{}), + exitCh: exitCh, resubmitIntervalCh: make(chan time.Duration), resubmitAdjustCh: make(chan *intervalAdjust, resubmitAdjustChanSize), interruptCommitFlag: config.CommitInterruptFlag, + coinbase: builderCoinbase, + flashbots: flashbots, } worker.noempty.Store(true) worker.profileCount = new(int32) @@ -383,12 +432,15 @@ func newWorker(config *Config, chainConfig *params.ChainConfig, engine consensus ctx := tracing.WithTracer(context.Background(), otel.GetTracerProvider().Tracer("MinerWorker")) - worker.wg.Add(4) - + worker.wg.Add(2) go worker.mainLoop(ctx) go worker.newWorkLoop(ctx, recommit) - go worker.resultLoop() - go worker.taskLoop() + if flashbots.algoType != ALGO_MEV_GETH || !flashbots.isFlashbots { + // only mine if not flashbots + worker.wg.Add(2) + go worker.resultLoop() + go worker.taskLoop() + } // Submit first work to initialize pending state. if init { @@ -528,9 +580,10 @@ func (w *worker) newWorkLoop(ctx context.Context, recommit time.Duration) { defer w.wg.Done() var ( - interrupt *atomic.Int32 - minRecommit = recommit // minimal resubmit interval specified by user. - timestamp int64 // timestamp for each round of sealing. + runningInterrupt *atomic.Int32 // Running task interrupt + queuedInterrupt *atomic.Int32 // Queued task interrupt + minRecommit = recommit // minimal resubmit interval specified by user. + timestamp int64 // timestamp for each round of sealing. ) timer := time.NewTimer(0) @@ -543,16 +596,31 @@ func (w *worker) newWorkLoop(ctx context.Context, recommit time.Duration) { ctx, span := tracing.Trace(ctx, "worker.newWorkLoop.commit") tracing.EndSpan(span) - if interrupt != nil { - interrupt.Store(s) + if runningInterrupt != nil { + runningInterrupt.Store(s) } - interrupt = new(atomic.Int32) + runningInterrupt = new(atomic.Int32) select { - case w.newWorkCh <- &newWorkReq{interrupt: interrupt, noempty: noempty, timestamp: timestamp, ctx: ctx}: + //case w.newWorkCh <- &newWorkReq{interrupt: interrupt, noempty: noempty, timestamp: timestamp, ctx: ctx}: case <-w.exitCh: return + case queuedRequest := <-w.newWorkCh: + // Previously queued request wasn't started yet, update the request and resubmit + queuedRequest.noempty = queuedRequest.noempty || noempty + queuedRequest.timestamp = timestamp + w.newWorkCh <- queuedRequest // guaranteed to be nonblocking + default: + // Previously queued request has already started, cycle interrupt pointer and submit new work + runningInterrupt = queuedInterrupt + queuedInterrupt = new(atomic.Int32) + w.newWorkCh <- &newWorkReq{interrupt: queuedInterrupt, noempty: noempty, timestamp: timestamp, ctx: ctx} // guaranteed to be nonblocking + } + + if runningInterrupt != nil && s > runningInterrupt.Load() { + runningInterrupt.Store(s) } + timer.Reset(recommit) w.newTxs.Store(0) } @@ -590,12 +658,12 @@ func (w *worker) newWorkLoop(ctx context.Context, recommit time.Duration) { // If sealing is running resubmit a new work cycle periodically to pull in // higher priced transactions. Disable this overhead for pending blocks. if w.IsRunning() && (w.chainConfig.Clique == nil || w.chainConfig.Clique.Period > 0) { + // flashbots: disable this because there can be new bundles // Short circuit if no new transaction arrives. - if w.newTxs.Load() == 0 { - timer.Reset(recommit) - continue - } - + //if atomic.LoadInt32(&w.newTxs) == 0 { + // timer.Reset(recommit) + // continue + //} commit(true, commitInterruptResubmit) } @@ -657,23 +725,20 @@ func (w *worker) mainLoop(ctx context.Context) { for { select { case req := <-w.newWorkCh: - if w.chainConfig.ChainID.Cmp(params.BorMainnetChainConfig.ChainID) == 0 || w.chainConfig.ChainID.Cmp(params.MumbaiChainConfig.ChainID) == 0 { - if w.eth.PeerCount() > 0 { - //nolint:contextcheck - w.commitWork(req.ctx, req.interrupt, req.noempty, req.timestamp) - } - } else { - //nolint:contextcheck + // Don't start if the work has already been interrupted + if req.interrupt == nil || req.interrupt.Load() == commitInterruptNone { w.commitWork(req.ctx, req.interrupt, req.noempty, req.timestamp) } case req := <-w.getWorkCh: - block, fees, err := w.generateWork(req.ctx, req.params) - req.result <- &newPayloadResult{ - err: err, - block: block, - fees: fees, - } + go func() { + block, fees, err := w.generateWork(req.ctx, req.params) + req.result <- &newPayloadResult{ + err: err, + block: block, + fees: fees, + } + }() case ev := <-w.chainSideCh: // Short circuit for duplicate side blocks if _, exist := w.localUncles[ev.Block.Hash()]; exist { @@ -740,7 +805,7 @@ func (w *worker) mainLoop(ctx context.Context) { baseFee = cmath.FromBig(w.current.header.BaseFee) } - txset := types.NewTransactionsByPriceAndNonce(w.current.signer, txs, baseFee) + txset := types.NewTransactionsByPriceAndNonce(w.current.signer, txs, nil, nil, baseFee) tcount := w.current.tcount //nolint:contextcheck @@ -783,6 +848,9 @@ func (w *worker) taskLoop() { var ( stopCh chan struct{} prev common.Hash + + prevParentHash common.Hash + prevProfit *big.Int ) // interrupt aborts the in-flight sealing task. @@ -804,6 +872,16 @@ func (w *worker) taskLoop() { if sealHash == prev { continue } + + taskParentHash := task.block.Header().ParentHash + // reject new tasks which don't profit + if taskParentHash == prevParentHash && + prevProfit != nil && task.profit.Cmp(prevProfit) < 0 { + continue + } + prevParentHash = taskParentHash + prevProfit = task.profit + // Interrupt previous sealing operation interrupt() @@ -964,6 +1042,7 @@ func (w *worker) makeEnv(parent *types.Header, header *types.Header, coinbase co family: mapset.NewSet[common.Hash](), header: header, uncles: make(map[common.Hash]*types.Header), + profit: new(big.Int), } // when 08 is processed ancestors contain 07 (quick block) for _, ancestor := range w.chain.GetBlocksFromHash(parent.Hash(), 7) { @@ -976,7 +1055,7 @@ func (w *worker) makeEnv(parent *types.Header, header *types.Header, coinbase co } // Keep track of transactions which return errors so they can be removed env.tcount = 0 - + env.gasPool = new(core.GasPool).AddGas(header.GasLimit) return env, nil } @@ -1026,29 +1105,40 @@ func (w *worker) updateSnapshot(env *environment) { func (w *worker) commitTransaction(env *environment, tx *types.Transaction, interruptCtx context.Context) ([]*types.Log, error) { var ( - snap = env.state.Snapshot() - gp = env.gasPool.Gas() + snap = env.state.Snapshot() + gasRemainingBefore = env.gasPool.Gas() + gasPool = *env.gasPool + envGasUsed = env.header.GasUsed ) // nolint : staticcheck interruptCtx = vm.SetCurrentTxOnContext(interruptCtx, tx.Hash()) - receipt, err := core.ApplyTransaction(w.chainConfig, w.chain, &env.coinbase, env.gasPool, env.state, env.header, tx, &env.header.GasUsed, *w.chain.GetVMConfig(), interruptCtx) + gasPrice, err := tx.EffectiveGasTip(env.header.BaseFee) + if err != nil { + return nil, err + } + + receipt, err := core.ApplyTransaction(w.chainConfig, w.chain, &env.coinbase, &gasPool, env.state, env.header, tx, &envGasUsed, *w.chain.GetVMConfig(), interruptCtx) if err != nil { env.state.RevertToSnapshot(snap) - env.gasPool.SetGas(gp) + env.gasPool.SetGas(gasRemainingBefore) return nil, err } + *env.gasPool = gasPool + env.header.GasUsed = envGasUsed env.txs = append(env.txs, tx) env.receipts = append(env.receipts, receipt) + gasUsed := new(big.Int).SetUint64(receipt.GasUsed) + env.profit.Add(env.profit, gasUsed.Mul(gasUsed, gasPrice)) + return receipt.Logs, nil } -//nolint:gocognit -func (w *worker) commitTransactions(env *environment, txs *types.TransactionsByPriceAndNonce, interrupt *atomic.Int32, interruptCtx context.Context) error { +func (w *worker) commitBundle(env *environment, txs types.Transactions, interrupt *atomic.Int32, interruptCtx context.Context) error { gasLimit := env.header.GasLimit if env.gasPool == nil { env.gasPool = new(core.GasPool).AddGas(gasLimit) @@ -1056,47 +1146,96 @@ func (w *worker) commitTransactions(env *environment, txs *types.TransactionsByP var coalescedLogs []*types.Log - var depsMVReadList [][]blockstm.ReadDescriptor - - var depsMVFullWriteList [][]blockstm.WriteDescriptor - - var mvReadMapList []map[blockstm.Key]blockstm.ReadDescriptor - - var deps map[int]map[int]bool - - chDeps := make(chan blockstm.TxDep) - - var count int + for _, tx := range txs { + // Check interruption signal and abort building if it's fired. + if interrupt != nil { + if signal := interrupt.Load(); signal != commitInterruptNone { + return signalToErr(signal) + } + } + // If we don't have enough gas for any further transactions discard the block + // since not all bundles of the were applied + if env.gasPool.Gas() < params.TxGas { + log.Trace("Not enough gas for further transactions", "have", env.gasPool, "want", params.TxGas) + return errCouldNotApplyTransaction + } - var depsWg sync.WaitGroup + // Error may be ignored here. The error has already been checked + // during transaction acceptance is the transaction pool. + // + // We use the eip155 signer regardless of the current hf. + from, _ := types.Sender(env.signer, tx) + // Check whether the tx is replay protected. If we're not in the EIP155 hf + // phase, start ignoring the sender until we do. + if tx.Protected() && !w.chainConfig.IsEIP155(env.header.Number) { + log.Trace("Ignoring reply protected transaction", "hash", tx.Hash(), "eip155", w.chainConfig.EIP155Block) + return errCouldNotApplyTransaction + } - EnableMVHashMap := false + logs, err := w.commitTransaction(env, tx, interruptCtx) + switch { + case errors.Is(err, core.ErrGasLimitReached): + // Pop the current out-of-gas transaction without shifting in the next from the account + log.Trace("Gas limit exceeded for current block", "sender", from) + return errCouldNotApplyTransaction - // create and add empty mvHashMap in statedb - if EnableMVHashMap { - depsMVReadList = [][]blockstm.ReadDescriptor{} + case errors.Is(err, core.ErrNonceTooLow): + // New head notification data race between the transaction pool and miner, shift + log.Trace("Skipping transaction with low nonce", "sender", from, "nonce", tx.Nonce()) + return errCouldNotApplyTransaction - depsMVFullWriteList = [][]blockstm.WriteDescriptor{} + case errors.Is(err, core.ErrNonceTooHigh): + // Reorg notification data race between the transaction pool and miner, skip account = + log.Trace("Skipping account with hight nonce", "sender", from, "nonce", tx.Nonce()) + return errCouldNotApplyTransaction - mvReadMapList = []map[blockstm.Key]blockstm.ReadDescriptor{} + case errors.Is(err, nil): + // Everything ok, collect the logs and shift in the next transaction from the same account + coalescedLogs = append(coalescedLogs, logs...) + env.tcount++ + continue - deps = map[int]map[int]bool{} + case errors.Is(err, core.ErrTxTypeNotSupported): + // Pop the unsupported transaction without shifting in the next from the account + log.Trace("Skipping unsupported transaction type", "sender", from, "type", tx.Type()) + return errCouldNotApplyTransaction - chDeps = make(chan blockstm.TxDep) + default: + // Strange error, discard the transaction and get the next in line (note, the + // nonce-too-high clause will prevent us from executing in vain). + log.Debug("Transaction failed, account skipped", "hash", tx.Hash(), "err", err) + return errCouldNotApplyTransaction + } + } - count = 0 + if !w.IsRunning() && len(coalescedLogs) > 0 { + // We don't push the pendingLogsEvent while we are sealing. The reason is that + // when we are sealing, the worker will regenerate a sealing block every 3 seconds. + // In order to avoid pushing the repeated pendingLog, we disable the pending log pushing. - depsWg.Add(1) + // make a copy, the state caches the logs and these logs get "upgraded" from pending to mined + // logs by filling in the block hash when the block was mined by the local miner. This can + // cause a race condition if a log was "upgraded" before the PendingLogsEvent is processed. + cpy := make([]*types.Log, len(coalescedLogs)) + for i, l := range coalescedLogs { + cpy[i] = new(types.Log) + *cpy[i] = *l + } + w.pendingLogsFeed.Send(cpy) + } - go func(chDeps chan blockstm.TxDep) { - for t := range chDeps { - deps = blockstm.UpdateDeps(deps, t) - } + return nil +} - depsWg.Done() - }(chDeps) +//nolint:gocognit +func (w *worker) commitTransactions(env *environment, txs *types.TransactionsByPriceAndNonce, interrupt *atomic.Int32, interruptCtx context.Context) error { + gasLimit := env.header.GasLimit + if env.gasPool == nil { + env.gasPool = new(core.GasPool).AddGas(gasLimit) } + var coalescedLogs []*types.Log + initialGasLimit := env.gasPool.Gas() initialTxs := txs.GetTxs() @@ -1116,9 +1255,6 @@ func (w *worker) commitTransactions(env *environment, txs *types.TransactionsByP mainloop: for { if interruptCtx != nil { - if EnableMVHashMap { - env.state.AddEmptyMVHashMap() - } // case of interrupting by timeout select { @@ -1144,7 +1280,12 @@ mainloop: break } // Retrieve the next transaction and abort if all done. - tx := txs.Peek() + // Retrieve the next transaction and abort if all done + order := txs.Peek() + if order == nil { + break + } + tx := order.Tx() if tx == nil { breakCause = "all transactions has been included" break @@ -1208,21 +1349,6 @@ mainloop: coalescedLogs = append(coalescedLogs, logs...) env.tcount++ - if EnableMVHashMap { - depsMVReadList = append(depsMVReadList, env.state.MVReadList()) - depsMVFullWriteList = append(depsMVFullWriteList, env.state.MVFullWriteList()) - mvReadMapList = append(mvReadMapList, env.state.MVReadMap()) - - temp := blockstm.TxDep{ - Index: env.tcount - 1, - ReadList: depsMVReadList[count], - FullWriteList: depsMVFullWriteList, - } - - chDeps <- temp - count++ - } - txs.Shift() log.OnDebug(func(lg log.Logging) { @@ -1235,73 +1361,6 @@ mainloop: log.Debug("Transaction failed, account skipped", "hash", tx.Hash(), "err", err) txs.Pop() } - - if EnableMVHashMap { - env.state.ClearReadMap() - env.state.ClearWriteMap() - } - } - - // nolint:nestif - if EnableMVHashMap && w.IsRunning() { - close(chDeps) - depsWg.Wait() - - var blockExtraData types.BlockExtraData - - tempVanity := env.header.Extra[:types.ExtraVanityLength] - tempSeal := env.header.Extra[len(env.header.Extra)-types.ExtraSealLength:] - - if len(mvReadMapList) > 0 { - tempDeps := make([][]uint64, len(mvReadMapList)) - - for j := range deps[0] { - tempDeps[0] = append(tempDeps[0], uint64(j)) - } - - delayFlag := true - - for i := 1; i <= len(mvReadMapList)-1; i++ { - reads := mvReadMapList[i-1] - - _, ok1 := reads[blockstm.NewSubpathKey(env.coinbase, state.BalancePath)] - _, ok2 := reads[blockstm.NewSubpathKey(common.HexToAddress(w.chainConfig.Bor.CalculateBurntContract(env.header.Number.Uint64())), state.BalancePath)] - - if ok1 || ok2 { - delayFlag = false - } - - for j := range deps[i] { - tempDeps[i] = append(tempDeps[i], uint64(j)) - } - } - - if err := rlp.DecodeBytes(env.header.Extra[types.ExtraVanityLength:len(env.header.Extra)-types.ExtraSealLength], &blockExtraData); err != nil { - log.Error("error while decoding block extra data", "err", err) - return err - } - - if delayFlag { - blockExtraData.TxDependency = tempDeps - } else { - blockExtraData.TxDependency = nil - } - } else { - blockExtraData.TxDependency = nil - } - - blockExtraDataBytes, err := rlp.EncodeToBytes(blockExtraData) - if err != nil { - log.Error("error while encoding block extra data: %v", err) - return err - } - - env.header.Extra = []byte{} - - env.header.Extra = append(tempVanity, blockExtraDataBytes...) - - env.header.Extra = append(env.header.Extra, tempSeal...) - } if !w.IsRunning() && len(coalescedLogs) > 0 { @@ -1329,67 +1388,65 @@ type generateParams struct { forceTime bool // Flag whether the given timestamp is immutable or not parentHash common.Hash // Parent block hash, empty means the latest chain head coinbase common.Address // The fee recipient address for including transaction + gasLimit uint64 // The validator's requested gas limit target random common.Hash // The randomness generated by beacon chain, empty before the merge withdrawals types.Withdrawals // List of withdrawals to include in block. noUncle bool // Flag whether the uncle block inclusion is allowed noTxs bool // Flag whether an empty block without any transaction is expected + onBlock BlockHookFn // Callback to call for each produced block } -// prepareWork constructs the sealing task according to the given parameters, -// either based on the last chain head or specified parent. In this function -// the pending transactions are not filled yet, only the empty task returned. -func (w *worker) prepareWork(genParams *generateParams) (*environment, error) { - w.mu.RLock() - defer w.mu.RUnlock() - +func doPrepareHeader(genParams *generateParams, chain *core.BlockChain, config *Config, chainConfig *params.ChainConfig, extra []byte, engine consensus.Engine) (*types.Header, *types.Header, error) { // Find the parent block for sealing task - parent := w.chain.CurrentBlock() - + parent := chain.CurrentBlock() if genParams.parentHash != (common.Hash{}) { - block := w.chain.GetBlockByHash(genParams.parentHash) + block := chain.GetBlockByHash(genParams.parentHash) if block == nil { - return nil, fmt.Errorf("missing parent") + return nil, nil, fmt.Errorf("missing parent") } - parent = block.Header() } + // Sanity check the timestamp correctness, recap the timestamp // to parent+1 if the mutation is allowed. timestamp := genParams.timestamp if parent.Time >= timestamp { if genParams.forceTime { - return nil, fmt.Errorf("invalid timestamp, parent %d given %d", parent.Time, timestamp) + return nil, nil, fmt.Errorf("invalid timestamp, parent %d given %d", parent.Time, timestamp) } - timestamp = parent.Time + 1 } - // Construct the sealing block header. + // Construct the sealing block header, set the extra field if it's allowed + gasTarget := genParams.gasLimit + if gasTarget == 0 { + gasTarget = config.GasCeil + } header := &types.Header{ ParentHash: parent.Hash(), Number: new(big.Int).Add(parent.Number, common.Big1), - GasLimit: core.CalcGasLimit(parent.GasLimit, w.config.GasCeil), + GasLimit: core.CalcGasLimit(parent.GasLimit, gasTarget), Time: timestamp, Coinbase: genParams.coinbase, } - // Set the extra field. - if len(w.extra) != 0 { - header.Extra = w.extra + if len(extra) != 0 { + header.Extra = extra } + // Set the randomness field from the beacon chain if it's available. if genParams.random != (common.Hash{}) { header.MixDigest = genParams.random } // Set baseFee and GasLimit if we are on an EIP-1559 chain - if w.chainConfig.IsLondon(header.Number) { - header.BaseFee = misc.CalcBaseFeeUint(w.chainConfig, parent).ToBig() - - if !w.chainConfig.IsLondon(parent.Number) { - parentGasLimit := parent.GasLimit * w.chainConfig.ElasticityMultiplier() - header.GasLimit = core.CalcGasLimit(parentGasLimit, w.config.GasCeil) + if chainConfig.IsLondon(header.Number) { + header.BaseFee = misc.CalcBaseFee(chainConfig, parent) + if !chainConfig.IsLondon(parent.Number) { + parentGasLimit := parent.GasLimit * chainConfig.ElasticityMultiplier() + header.GasLimit = core.CalcGasLimit(parentGasLimit, gasTarget) } } // Run the consensus preparation with the default or customized consensus engine. - if err := w.engine.Prepare(w.chain, header); err != nil { + // Run the consensus preparation with the default or customized consensus engine. + if err := engine.Prepare(chain, header); err != nil { switch err.(type) { case *bor.UnauthorizedSignerError: log.Debug("Failed to prepare header for sealing", "err", err) @@ -1397,8 +1454,26 @@ func (w *worker) prepareWork(genParams *generateParams) (*environment, error) { log.Error("Failed to prepare header for sealing", "err", err) } + return nil, nil, err + } + + return header, parent, nil +} + +// prepareWork constructs the sealing task according to the given parameters, +// either based on the last chain head or specified parent. In this function +// the pending transactions are not filled yet, only the empty task returned. +func (w *worker) prepareWork(genParams *generateParams) (*environment, error) { + w.mu.RLock() + defer w.mu.RUnlock() + + header, parent, err := doPrepareHeader(genParams, w.chain, w.config, w.chainConfig, w.extra, w.engine) + if err != nil { return nil, err } + // uncomment to enable dirty fix for clique coinbase for local builder + //header.Coinbase = genParams.coinbase + // Could potentially happen if starting to mine in an odd state. // Note genParams.coinbase can be different with header.Coinbase // since clique algorithm can modify the coinbase field in header. @@ -1430,68 +1505,24 @@ func (w *worker) prepareWork(genParams *generateParams) (*environment, error) { return env, nil } -func startProfiler(profile string, filepath string, number uint64) (func() error, error) { +func (w *worker) fillTransactionsSelectAlgo(interrupt *atomic.Int32, env *environment, interruptCtx context.Context) ([]types.SimulatedBundle, []types.SimulatedBundle, []types.UsedSBundle, map[common.Hash]struct{}, error) { var ( - buf bytes.Buffer - err error + blockBundles []types.SimulatedBundle + allBundles []types.SimulatedBundle + usedSbundles []types.UsedSBundle + mempoolTxHashes map[common.Hash]struct{} + err error ) - - closeFn := func() {} - - switch profile { - case "cpu": - err = pprof.StartCPUProfile(&buf) - - if err == nil { - closeFn = func() { - pprof.StopCPUProfile() - } - } - case "trace": - err = ptrace.Start(&buf) - - if err == nil { - closeFn = func() { - ptrace.Stop() - } - } - case "heap": - runtime.GC() - - err = pprof.WriteHeapProfile(&buf) + ctx := context.Background() + switch w.flashbots.algoType { + case ALGO_GREEDY, ALGO_GREEDY_BUCKETS, ALGO_GREEDY_MULTISNAP, ALGO_GREEDY_BUCKETS_MULTISNAP: + blockBundles, allBundles, usedSbundles, mempoolTxHashes, err = w.fillTransactionsAlgoWorker(interrupt, env) + case ALGO_MEV_GETH: + blockBundles, allBundles, mempoolTxHashes, err = w.fillTransactions(ctx, interrupt, env, interruptCtx) default: - log.Info("Incorrect profile name") - } - - if err != nil { - return func() error { - closeFn() - return nil - }, err - } - - closeFnNew := func() error { - var err error - - closeFn() - - if buf.Len() == 0 { - return nil - } - - f, err := os.Create(filepath + "/" + profile + "-" + fmt.Sprint(number) + ".prof") - if err != nil { - return err - } - - defer f.Close() - - _, err = f.Write(buf.Bytes()) - - return err + blockBundles, allBundles, mempoolTxHashes, err = w.fillTransactions(ctx, interrupt, env, interruptCtx) } - - return closeFnNew, nil + return blockBundles, allBundles, usedSbundles, mempoolTxHashes, err } // fillTransactions retrieves the pending transactions from the txpool and fills them @@ -1499,137 +1530,86 @@ func startProfiler(profile string, filepath string, number uint64) (func() error // be customized with the plugin in the future. // //nolint:gocognit -func (w *worker) fillTransactions(ctx context.Context, interrupt *atomic.Int32, env *environment, interruptCtx context.Context) error { +func (w *worker) fillTransactions(ctx context.Context, interrupt *atomic.Int32, env *environment, interruptCtx context.Context) ([]types.SimulatedBundle, []types.SimulatedBundle, map[common.Hash]struct{}, error) { ctx, span := tracing.StartSpan(ctx, "fillTransactions") defer tracing.EndSpan(span) // Split the pending transactions into locals and remotes // Fill the block with all available pending transactions. pending := w.eth.TxPool().Pending(ctx, true) + mempoolTxHashes := make(map[common.Hash]struct{}, len(pending)) + for _, txs := range pending { + for _, tx := range txs { + mempoolTxHashes[tx.Hash()] = struct{}{} + } + } localTxs, remoteTxs := make(map[common.Address]types.Transactions), pending + for _, account := range w.eth.TxPool().Locals() { + if txs := remoteTxs[account]; len(txs) > 0 { + delete(remoteTxs, account) + localTxs[account] = txs + } + } - var ( - localTxsCount int - remoteTxsCount int - ) + var blockBundles []types.SimulatedBundle + var allBundles []types.SimulatedBundle + if w.flashbots.isFlashbots { + bundles, ccBundleCh := w.eth.TxPool().MevBundles(env.header.Number, env.header.Time) + bundles = append(bundles, <-ccBundleCh...) - // TODO: move to config or RPC - const profiling = false + var ( + bundleTxs types.Transactions + resultingBundle simulatedBundle + mergedBundles []types.SimulatedBundle + numBundles int + err error + ) + // Sets allBundles in outer scope + bundleTxs, resultingBundle, mergedBundles, numBundles, allBundles, err = w.generateFlashbotsBundle(env, bundles, pending) + if err != nil { + log.Error("Failed to generate flashbots bundle", "err", err) + return nil, nil, nil, err + } + log.Info("Flashbots bundle", "ethToCoinbase", ethIntToFloat(resultingBundle.TotalEth), "gasUsed", resultingBundle.TotalGasUsed, "bundleScore", resultingBundle.MevGasPrice, "bundleLength", len(bundleTxs), "numBundles", numBundles, "worker", w.flashbots.maxMergedBundles) + if len(bundleTxs) == 0 { + return nil, nil, nil, errors.New("no bundles to apply") + } + if err := w.commitBundle(env, bundleTxs, interrupt, interruptCtx); err != nil { + return nil, nil, nil, err + } + blockBundles = mergedBundles + env.profit.Add(env.profit, resultingBundle.EthSentToCoinbase) + } - if profiling { - doneCh := make(chan struct{}) + var ( + localEnvTCount int + remoteEnvTCount int + err error + ) - defer func() { - close(doneCh) - }() + if len(localTxs) > 0 { + var txs *types.TransactionsByPriceAndNonce - go func(number uint64) { - closeFn := func() error { - return nil + tracing.Exec(ctx, "", "worker.LocalTransactionsByPriceAndNonce", func(ctx context.Context, span trace.Span) { + var baseFee *uint256.Int + if env.header.BaseFee != nil { + baseFee = cmath.FromBig(env.header.BaseFee) } - for { - select { - case <-time.After(150 * time.Millisecond): - // Check if we've not crossed limit - if attempt := atomic.AddInt32(w.profileCount, 1); attempt >= 10 { - log.Info("Completed profiling", "attempt", attempt) + txs = types.NewTransactionsByPriceAndNonce(env.signer, localTxs, nil, nil, baseFee) - return - } - - log.Info("Starting profiling in fill transactions", "number", number) - - dir, err := os.MkdirTemp("", fmt.Sprintf("bor-traces-%s-", time.Now().UTC().Format("2006-01-02-150405Z"))) - if err != nil { - log.Error("Error in profiling", "path", dir, "number", number, "err", err) - return - } - - // grab the cpu profile - closeFnInternal, err := startProfiler("cpu", dir, number) - if err != nil { - log.Error("Error in profiling", "path", dir, "number", number, "err", err) - return - } - - closeFn = func() error { - err := closeFnInternal() - - log.Info("Completed profiling", "path", dir, "number", number, "error", err) - - return nil - } - - case <-doneCh: - err := closeFn() - - if err != nil { - log.Info("closing fillTransactions", "number", number, "error", err) - } - - return - } - } - }(env.header.Number.Uint64()) - } - - tracing.Exec(ctx, "", "worker.SplittingTransactions", func(ctx context.Context, span trace.Span) { - prePendingTime := time.Now() - - pending := w.eth.TxPool().Pending(ctx, true) - remoteTxs = pending - - postPendingTime := time.Now() - - for _, account := range w.eth.TxPool().Locals() { - if txs := remoteTxs[account]; len(txs) > 0 { - delete(remoteTxs, account) - - localTxs[account] = txs - } - } - - postLocalsTime := time.Now() - - tracing.SetAttributes( - span, - attribute.Int("len of local txs", localTxsCount), - attribute.Int("len of remote txs", remoteTxsCount), - attribute.String("time taken by Pending()", fmt.Sprintf("%v", postPendingTime.Sub(prePendingTime))), - attribute.String("time taken by Locals()", fmt.Sprintf("%v", postLocalsTime.Sub(postPendingTime))), - ) - }) - - var ( - localEnvTCount int - remoteEnvTCount int - err error - ) - - if len(localTxs) > 0 { - var txs *types.TransactionsByPriceAndNonce - - tracing.Exec(ctx, "", "worker.LocalTransactionsByPriceAndNonce", func(ctx context.Context, span trace.Span) { - var baseFee *uint256.Int - if env.header.BaseFee != nil { - baseFee = cmath.FromBig(env.header.BaseFee) - } - - txs = types.NewTransactionsByPriceAndNonce(env.signer, localTxs, baseFee) - - tracing.SetAttributes( - span, - attribute.Int("len of tx local Heads", txs.GetTxs()), - ) - }) + tracing.SetAttributes( + span, + attribute.Int("len of tx local Heads", txs.GetTxs()), + ) + }) tracing.Exec(ctx, "", "worker.LocalCommitTransactions", func(ctx context.Context, span trace.Span) { err = w.commitTransactions(env, txs, interrupt, interruptCtx) }) if err != nil { - return err + return nil, nil, nil, err } localEnvTCount = env.tcount @@ -1644,7 +1624,7 @@ func (w *worker) fillTransactions(ctx context.Context, interrupt *atomic.Int32, baseFee = cmath.FromBig(env.header.BaseFee) } - txs = types.NewTransactionsByPriceAndNonce(env.signer, remoteTxs, baseFee) + txs = types.NewTransactionsByPriceAndNonce(env.signer, remoteTxs, nil, nil, baseFee) tracing.SetAttributes( span, @@ -1657,7 +1637,7 @@ func (w *worker) fillTransactions(ctx context.Context, interrupt *atomic.Int32, }) if err != nil { - return err + return nil, nil, nil, err } remoteEnvTCount = env.tcount @@ -1669,40 +1649,266 @@ func (w *worker) fillTransactions(ctx context.Context, interrupt *atomic.Int32, attribute.Int("len of final remote txs", remoteEnvTCount), ) - return nil + return blockBundles, allBundles, mempoolTxHashes, nil +} + +// fillTransactionsAlgoWorker retrieves the pending transactions and bundles from the txpool and fills them +// into the given sealing block. +// Returns error if any, otherwise the bundles that made it into the block and all bundles that passed simulation +func (w *worker) fillTransactionsAlgoWorker(interrupt *atomic.Int32, env *environment) ([]types.SimulatedBundle, []types.SimulatedBundle, []types.UsedSBundle, map[common.Hash]struct{}, error) { + // Split the pending transactions into locals and remotes + // Fill the block with all available pending transactions. + pending := w.eth.TxPool().Pending(context.Background(), true) + mempoolTxHashes := make(map[common.Hash]struct{}, len(pending)) + for _, txs := range pending { + for _, tx := range txs { + mempoolTxHashes[tx.Hash()] = struct{}{} + } + } + bundlesToConsider, sbundlesToConsider, err := w.getSimulatedBundles(env) + if err != nil { + return nil, nil, nil, nil, err + } + + var ( + newEnv *environment + blockBundles []types.SimulatedBundle + usedSbundle []types.UsedSBundle + ) + switch w.flashbots.algoType { + case ALGO_GREEDY_BUCKETS: + priceCutoffPercent := w.config.PriceCutoffPercent + if !(priceCutoffPercent >= 0 && priceCutoffPercent <= 100) { + return nil, nil, nil, nil, errors.New("invalid price cutoff percent - must be between 0 and 100") + } + + algoConf := &algorithmConfig{ + DropRevertibleTxOnErr: w.config.DiscardRevertibleTxOnErr, + EnforceProfit: true, + ProfitThresholdPercent: defaultProfitThresholdPercent, + PriceCutoffPercent: priceCutoffPercent, + } + builder := newGreedyBucketsBuilder( + w.chain, w.chainConfig, algoConf, w.blockList, env, + w.config.BuilderTxSigningKey, interrupt, + ) + + newEnv, blockBundles, usedSbundle = builder.buildBlock(bundlesToConsider, sbundlesToConsider, pending) + case ALGO_GREEDY_BUCKETS_MULTISNAP: + priceCutoffPercent := w.config.PriceCutoffPercent + if !(priceCutoffPercent >= 0 && priceCutoffPercent <= 100) { + return nil, nil, nil, nil, errors.New("invalid price cutoff percent - must be between 0 and 100") + } + + algoConf := &algorithmConfig{ + DropRevertibleTxOnErr: w.config.DiscardRevertibleTxOnErr, + EnforceProfit: true, + ProfitThresholdPercent: defaultProfitThresholdPercent, + PriceCutoffPercent: priceCutoffPercent, + } + builder := newGreedyBucketsMultiSnapBuilder( + w.chain, w.chainConfig, algoConf, w.blockList, env, + w.config.BuilderTxSigningKey, interrupt, + ) + newEnv, blockBundles, usedSbundle = builder.buildBlock(bundlesToConsider, sbundlesToConsider, pending) + case ALGO_GREEDY_MULTISNAP: + // For greedy multi-snap builder, set algorithm configuration to default values, + // except DropRevertibleTxOnErr which is passed in from worker config + algoConf := &algorithmConfig{ + DropRevertibleTxOnErr: w.config.DiscardRevertibleTxOnErr, + EnforceProfit: defaultAlgorithmConfig.EnforceProfit, + ProfitThresholdPercent: defaultAlgorithmConfig.ProfitThresholdPercent, + } + + builder := newGreedyMultiSnapBuilder( + w.chain, w.chainConfig, algoConf, w.blockList, env, + w.config.BuilderTxSigningKey, interrupt, + ) + newEnv, blockBundles, usedSbundle = builder.buildBlock(bundlesToConsider, sbundlesToConsider, pending) + case ALGO_GREEDY: + fallthrough + default: + // For default greedy builder, set algorithm configuration to default values, + // except DropRevertibleTxOnErr which is passed in from worker config + algoConf := &algorithmConfig{ + DropRevertibleTxOnErr: w.config.DiscardRevertibleTxOnErr, + EnforceProfit: defaultAlgorithmConfig.EnforceProfit, + ProfitThresholdPercent: defaultAlgorithmConfig.ProfitThresholdPercent, + } + + builder := newGreedyBuilder( + w.chain, w.chainConfig, algoConf, w.blockList, + env, w.config.BuilderTxSigningKey, interrupt, + ) + newEnv, blockBundles, usedSbundle = builder.buildBlock(bundlesToConsider, sbundlesToConsider, pending) + } + + *env = *newEnv + + return blockBundles, bundlesToConsider, usedSbundle, mempoolTxHashes, err +} + +func (w *worker) getSimulatedBundles(env *environment) ([]types.SimulatedBundle, []*types.SimSBundle, error) { + if !w.flashbots.isFlashbots { + return nil, nil, nil + } + + bundles, ccBundlesCh := w.eth.TxPool().MevBundles(env.header.Number, env.header.Time) + sbundles := w.eth.TxPool().GetSBundles(env.header.Number) + + // TODO: consider interrupt + simBundles, simSBundles, err := w.simulateBundles(env, bundles, sbundles, nil) /* do not consider gas impact of mempool txs as bundles are treated as transactions wrt ordering */ + if err != nil { + log.Error("Failed to simulate bundles", "err", err) + return nil, nil, err + } + + ccBundles := <-ccBundlesCh + if ccBundles == nil { + return simBundles, simSBundles, nil + } + + simCcBundles, _, err := w.simulateBundles(env, ccBundles, nil, nil) /* do not consider gas impact of mempool txs as bundles are treated as transactions wrt ordering */ + if err != nil { + log.Error("Failed to simulate cc bundles", "err", err) + return simBundles, simSBundles, nil + } + + return append(simBundles, simCcBundles...), simSBundles, nil } // generateWork generates a sealing block based on the given parameters. func (w *worker) generateWork(ctx context.Context, params *generateParams) (*types.Block, *big.Int, error) { + start := time.Now() + validatorCoinbase := params.coinbase + // Set builder coinbase to be passed to beacon header + params.coinbase = w.coinbase + work, err := w.prepareWork(params) if err != nil { return nil, nil, err } defer work.discard() + finalizeFn := func(env *environment, orderCloseTime time.Time, + blockBundles []types.SimulatedBundle, allBundles []types.SimulatedBundle, usedSbundles []types.UsedSBundle, noTxs bool) (*types.Block, *big.Int, error) { + block, profit, err := w.finalizeBlock(ctx, env, params.withdrawals, validatorCoinbase, noTxs) + if err != nil { + log.Error("could not finalize block", "err", err) + return nil, nil, err + } + + var okSbundles, totalSbundles int + for _, sb := range usedSbundles { + if sb.Success { + okSbundles++ + } + totalSbundles++ + } + + log.Info("Block finalized and assembled", + "height", block.Number().String(), "blockProfit", ethIntToFloat(profit), + "txs", len(env.txs), "bundles", len(blockBundles), "okSbundles", okSbundles, "totalSbundles", totalSbundles, + "gasUsed", block.GasUsed(), "time", time.Since(start)) + + if params.onBlock != nil { + go params.onBlock(block, profit, orderCloseTime, blockBundles, allBundles, usedSbundles) + } + + return block, profit, nil + } + + if params.noTxs { + return finalizeFn(work, time.Now(), nil, nil, nil, true) + } + // nolint : contextcheck - var interruptCtx = context.Background() + paymentTxReserve, err := w.proposerTxPrepare(work, &validatorCoinbase) + if err != nil { + return nil, nil, err + } - if !params.noTxs { - interrupt := new(atomic.Int32) + orderCloseTime := time.Now() - timer := time.AfterFunc(w.newpayloadTimeout, func() { - interrupt.Store(commitInterruptTimeout) - }) - defer timer.Stop() + blockBundles, allBundles, usedSbundles, mempoolTxHashes, err := w.fillTransactionsSelectAlgo(nil, work, nil) + if err != nil { + return nil, nil, err + } - err := w.fillTransactions(ctx, interrupt, work, interruptCtx) - if errors.Is(err, errBlockInterruptedByTimeout) { - log.Warn("Block building is interrupted", "allowance", common.PrettyDuration(w.newpayloadTimeout)) + // We mark transactions created by the builder as mempool transactions so code validating bundles will not fail + // for transactions created by the builder such as mev share refunds. + for _, tx := range work.txs { + from, err := types.Sender(work.signer, tx) + if err != nil { + return nil, nil, err } + if from == work.coinbase { + mempoolTxHashes[tx.Hash()] = struct{}{} + } + } + + // Let's ignore this nonsense + //err = VerifyBundlesAtomicity(work, blockBundles, allBundles, usedSbundles, mempoolTxHashes) + //if err != nil { + // log.Error("Bundle invariant is violated for built block", "block", work.header.Number, "err", err) + // return nil, nil, err + //} + + // no bundles or tx from mempool + if len(work.txs) == 0 { + return finalizeFn(work, orderCloseTime, blockBundles, allBundles, usedSbundles, true) } - block, err := w.engine.FinalizeAndAssemble(ctx, w.chain, work.header, work.state, work.txs, work.unclelist(), work.receipts, params.withdrawals) + err = w.proposerTxCommit(work, &validatorCoinbase, paymentTxReserve) if err != nil { return nil, nil, err } - return block, totalFees(block, work.receipts), nil + return finalizeFn(work, orderCloseTime, blockBundles, allBundles, usedSbundles, false) +} + +func (w *worker) finalizeBlock(ctx context.Context, work *environment, withdrawals types.Withdrawals, validatorCoinbase common.Address, noTxs bool) (*types.Block, *big.Int, error) { + block, err := w.engine.FinalizeAndAssemble(ctx, w.chain, work.header, work.state, work.txs, work.unclelist(), work.receipts, withdrawals) + if err != nil { + return nil, nil, err + } + + if w.config.BuilderTxSigningKey == nil { + return block, big.NewInt(0), nil + } + + if noTxs { + return block, big.NewInt(0), nil + } + + blockProfit, err := w.checkProposerPayment(work, validatorCoinbase) + if err != nil { + return nil, nil, err + } + + return block, blockProfit, nil +} + +func (w *worker) checkProposerPayment(work *environment, validatorCoinbase common.Address) (*big.Int, error) { + if len(work.txs) == 0 { + return nil, errors.New("no proposer payment tx") + } else if len(work.receipts) == 0 { + return nil, errors.New("no proposer payment receipt") + } + + lastTx := work.txs[len(work.txs)-1] + receipt := work.receipts[len(work.receipts)-1] + if receipt.TxHash != lastTx.Hash() || receipt.Status != types.ReceiptStatusSuccessful { + log.Error("proposer payment not successful!", "lastTx", lastTx, "receipt", receipt) + return nil, errors.New("last transaction is not proposer payment") + } + lastTxTo := lastTx.To() + if lastTxTo == nil || *lastTxTo != validatorCoinbase { + log.Error("last transaction is not to the proposer!", "lastTx", lastTx) + return nil, errors.New("last transaction is not proposer payment") + } + + return new(big.Int).Set(lastTx.Value()), nil } // commitWork generates several new sealing tasks based on the parent block @@ -1764,9 +1970,9 @@ func (w *worker) commitWork(ctx context.Context, interrupt *atomic.Int32, noempt if !noempty && !w.noempty.Load() { _ = w.commit(ctx, work.copy(), nil, false, start) } - // Fill pending transactions from the txpool into the block. - err = w.fillTransactions(ctx, interrupt, work, interruptCtx) + // Fill pending transactions from the txpool + _, _, _, _, err = w.fillTransactionsSelectAlgo(interrupt, work, nil) switch { case err == nil: // The entire block is filled, decrease resubmit interval in case @@ -1865,12 +2071,13 @@ func (w *worker) commit(ctx context.Context, env *environment, interval func(), case w.taskCh <- &task{ctx: ctx, receipts: env.receipts, state: env.state, block: block, createdAt: time.Now()}: w.unconfirmed.Shift(block.NumberU64() - 1) - fees := totalFees(block, env.receipts) + fees := totalFees(block, env) feesInEther := new(big.Float).Quo(new(big.Float).SetInt(fees), big.NewFloat(params.Ether)) log.Info("Commit new sealing work", "number", block.Number(), "sealhash", w.engine.SealHash(block.Header()), "uncles", len(env.uncles), "txs", env.tcount, "gas", block.GasUsed(), "fees", feesInEther, - "elapsed", common.PrettyDuration(time.Since(start))) + "elapsed", common.PrettyDuration(time.Since(start)), "isFlashbots", w.flashbots.isFlashbots, + "worker", w.flashbots.maxMergedBundles) case <-w.exitCh: log.Info("Worker has exited") @@ -1888,7 +2095,8 @@ func (w *worker) commit(ctx context.Context, env *environment, interval func(), // getSealingBlock generates the sealing block based on the given parameters. // The generation result will be passed back via the given channel no matter // the generation itself succeeds or not. -func (w *worker) getSealingBlock(parent common.Hash, timestamp uint64, coinbase common.Address, random common.Hash, withdrawals types.Withdrawals, noTxs bool) (*types.Block, *big.Int, error) { +func (w *worker) getSealingBlock(parent common.Hash, timestamp uint64, coinbase common.Address, gasLimit uint64, random common.Hash, + withdrawals types.Withdrawals, noTxs bool, blockHook BlockHookFn) (*types.Block, *big.Int, error) { ctx := tracing.WithTracer(context.Background(), otel.GetTracerProvider().Tracer("getSealingBlock")) req := &getWorkReq{ @@ -1897,10 +2105,12 @@ func (w *worker) getSealingBlock(parent common.Hash, timestamp uint64, coinbase forceTime: true, parentHash: parent, coinbase: coinbase, + gasLimit: gasLimit, random: random, withdrawals: withdrawals, noUncle: true, noTxs: noTxs, + onBlock: blockHook, }, result: make(chan *newPayloadResult, 1), ctx: ctx, @@ -1925,6 +2135,262 @@ func (w *worker) isTTDReached(header *types.Header) bool { return td != nil && ttd != nil && td.Cmp(ttd) >= 0 } +type simulatedBundle = types.SimulatedBundle + +func (w *worker) generateFlashbotsBundle(env *environment, bundles []types.MevBundle, pendingTxs map[common.Address]types.Transactions) (types.Transactions, simulatedBundle, []types.SimulatedBundle, int, []types.SimulatedBundle, error) { + simulatedBundles, _, err := w.simulateBundles(env, bundles, nil, pendingTxs) + if err != nil { + return nil, simulatedBundle{}, nil, 0, nil, err + } + + sort.SliceStable(simulatedBundles, func(i, j int) bool { + return simulatedBundles[j].MevGasPrice.Cmp(simulatedBundles[i].MevGasPrice) < 0 + }) + + bundleTxs, bundle, mergedBundles, numBundles, err := w.mergeBundles(env, simulatedBundles, pendingTxs) + return bundleTxs, bundle, mergedBundles, numBundles, simulatedBundles, err +} + +func (w *worker) mergeBundles(env *environment, bundles []simulatedBundle, pendingTxs map[common.Address]types.Transactions) (types.Transactions, simulatedBundle, []types.SimulatedBundle, int, error) { + mergedBundles := []types.SimulatedBundle{} + finalBundle := types.Transactions{} + + currentState := env.state.Copy() + gasPool := new(core.GasPool).AddGas(env.header.GasLimit) + + var prevState *state.StateDB + var prevGasPool *core.GasPool + + mergedBundle := simulatedBundle{ + TotalEth: new(big.Int), + EthSentToCoinbase: new(big.Int), + } + + count := 0 + for _, bundle := range bundles { + prevState = currentState.Copy() + prevGasPool = new(core.GasPool).AddGas(gasPool.Gas()) + + // the floor gas price is 99/100 what was simulated at the top of the block + floorGasPrice := new(big.Int).Mul(bundle.MevGasPrice, big.NewInt(99)) + floorGasPrice = floorGasPrice.Div(floorGasPrice, big.NewInt(100)) + + simmed, err := w.computeBundleGas(env, bundle.OriginalBundle, currentState, gasPool, pendingTxs, len(finalBundle)) + if err != nil || simmed.MevGasPrice.Cmp(floorGasPrice) <= 0 { + currentState = prevState + gasPool = prevGasPool + continue + } + + log.Info("Included bundle", "ethToCoinbase", ethIntToFloat(simmed.TotalEth), "gasUsed", simmed.TotalGasUsed, "bundleScore", simmed.MevGasPrice, "bundleLength", len(simmed.OriginalBundle.Txs), "worker", w.flashbots.maxMergedBundles) + mergedBundles = append(mergedBundles, simmed) + finalBundle = append(finalBundle, bundle.OriginalBundle.Txs...) + mergedBundle.TotalEth.Add(mergedBundle.TotalEth, simmed.TotalEth) + mergedBundle.EthSentToCoinbase.Add(mergedBundle.EthSentToCoinbase, simmed.EthSentToCoinbase) + mergedBundle.TotalGasUsed += simmed.TotalGasUsed + count++ + + if count >= w.flashbots.maxMergedBundles { + break + } + } + + if len(finalBundle) == 0 || count != w.flashbots.maxMergedBundles { + return nil, simulatedBundle{}, nil, count, nil + } + + return finalBundle, simulatedBundle{ + MevGasPrice: new(big.Int).Div(mergedBundle.TotalEth, new(big.Int).SetUint64(mergedBundle.TotalGasUsed)), + TotalEth: mergedBundle.TotalEth, + EthSentToCoinbase: mergedBundle.EthSentToCoinbase, + TotalGasUsed: mergedBundle.TotalGasUsed, + }, mergedBundles, count, nil +} + +func (w *worker) simulateBundles(env *environment, bundles []types.MevBundle, sbundles []*types.SBundle, pendingTxs map[common.Address]types.Transactions) ([]simulatedBundle, []*types.SimSBundle, error) { + start := time.Now() + headerHash := env.header.Hash() + simCache := w.flashbots.bundleCache.GetBundleCache(headerHash) + + simResult := make([]*simulatedBundle, len(bundles)) + sbSimResult := make([]*types.SimSBundle, len(sbundles)) + + var wg sync.WaitGroup + for i, bundle := range bundles { + if simmed, ok := simCache.GetSimulatedBundle(bundle.Hash); ok { + simResult[i] = simmed + continue + } + + wg.Add(1) + go func(idx int, bundle types.MevBundle, state *state.StateDB) { + defer wg.Done() + + if len(bundle.Txs) == 0 { + return + } + gasPool := new(core.GasPool).AddGas(env.header.GasLimit) + simmed, err := w.computeBundleGas(env, bundle, state, gasPool, pendingTxs, 0) + + if err != nil { + log.Trace("Error computing gas for a bundle", "error", err) + return + } + simResult[idx] = &simmed + + }(i, bundle, env.state.Copy()) + } + + for i, sbundle := range sbundles { + if simmed, ok := simCache.GetSimSBundle(sbundle.Hash()); ok { + sbSimResult[i] = simmed + continue + } + + wg.Add(1) + go func(idx int, sbundle *types.SBundle, state *state.StateDB) { + defer wg.Done() + + gp := new(core.GasPool).AddGas(env.header.GasLimit) + + tmpGasUsed := uint64(0) + config := *w.chain.GetVMConfig() + + simRes, err := core.SimBundle(w.chainConfig, w.chain, &env.coinbase, gp, state, env.header, sbundle, 0, &tmpGasUsed, config, false) + if err != nil { + return + } + + result := &types.SimSBundle{ + Bundle: sbundle, + MevGasPrice: simRes.MevGasPrice, + Profit: simRes.TotalProfit, + } + sbSimResult[idx] = result + + }(i, sbundle, env.state.Copy()) + } + + wg.Wait() + + simCache.UpdateSimulatedBundles(simResult, bundles) + simulatedBundles := make([]simulatedBundle, 0, len(bundles)) + for _, bundle := range simResult { + if bundle != nil { + simulatedBundles = append(simulatedBundles, *bundle) + } + } + + simCache.UpdateSimSBundle(sbSimResult, sbundles) + simulatedSbundle := make([]*types.SimSBundle, 0, len(sbundles)) + for _, sbundle := range sbSimResult { + if sbundle != nil { + simulatedSbundle = append(simulatedSbundle, sbundle) + } + } + + log.Debug("Simulated bundles", "block", env.header.Number, "allBundles", len(bundles), "okBundles", len(simulatedBundles), + "allSbundles", len(sbundles), "okSbundles", len(simulatedSbundle), "time", time.Since(start)) + return simulatedBundles, simulatedSbundle, nil +} + +func containsHash(arr []common.Hash, match common.Hash) bool { + for _, elem := range arr { + if elem == match { + return true + } + } + return false +} + +// Compute the adjusted gas price for a whole bundle +// Done by calculating all gas spent, adding transfers to the coinbase, and then dividing by gas used +func (w *worker) computeBundleGas( + env *environment, bundle types.MevBundle, state *state.StateDB, gasPool *core.GasPool, + pendingTxs map[common.Address]types.Transactions, currentTxCount int, +) (simulatedBundle, error) { + var totalGasUsed uint64 = 0 + var tempGasUsed uint64 + gasFees := new(big.Int) + + ethSentToCoinbase := new(big.Int) + + for i, tx := range bundle.Txs { + if env.header.BaseFee != nil && tx.Type() == 2 { + // Sanity check for extremely large numbers + if tx.GasFeeCap().BitLen() > 256 { + return simulatedBundle{}, core.ErrFeeCapVeryHigh + } + if tx.GasTipCap().BitLen() > 256 { + return simulatedBundle{}, core.ErrTipVeryHigh + } + // Ensure gasFeeCap is greater than or equal to gasTipCap. + if tx.GasFeeCapIntCmp(tx.GasTipCap()) < 0 { + return simulatedBundle{}, core.ErrTipAboveFeeCap + } + } + + state.SetTxContext(tx.Hash(), i+currentTxCount) + coinbaseBalanceBefore := state.GetBalance(env.coinbase) + + config := *w.chain.GetVMConfig() + + receipt, err := core.ApplyTransaction(w.chainConfig, w.chain, &env.coinbase, gasPool, state, env.header, tx, &tempGasUsed, config, nil) + if err != nil { + return simulatedBundle{}, err + } + if receipt.Status == types.ReceiptStatusFailed && !containsHash(bundle.RevertingTxHashes, receipt.TxHash) { + return simulatedBundle{}, errors.New("failed tx") + } + + totalGasUsed += receipt.GasUsed + + from, err := types.Sender(env.signer, tx) + if err != nil { + return simulatedBundle{}, err + } + + txInPendingPool := false + if accountTxs, ok := pendingTxs[from]; ok { + // check if tx is in pending pool + txNonce := tx.Nonce() + + for _, accountTx := range accountTxs { + if accountTx.Nonce() == txNonce { + txInPendingPool = true + break + } + } + } + + gasUsed := new(big.Int).SetUint64(receipt.GasUsed) + gasPrice, err := tx.EffectiveGasTip(env.header.BaseFee) + if err != nil { + return simulatedBundle{}, err + } + gasFeesTx := gasUsed.Mul(gasUsed, gasPrice) + coinbaseBalanceAfter := state.GetBalance(env.coinbase) + coinbaseDelta := big.NewInt(0).Sub(coinbaseBalanceAfter, coinbaseBalanceBefore) + coinbaseDelta.Sub(coinbaseDelta, gasFeesTx) + ethSentToCoinbase.Add(ethSentToCoinbase, coinbaseDelta) + + if !txInPendingPool { + // If tx is not in pending pool, count the gas fees + gasFees.Add(gasFees, gasFeesTx) + } + } + + totalEth := new(big.Int).Add(ethSentToCoinbase, gasFees) + + return simulatedBundle{ + MevGasPrice: new(big.Int).Div(totalEth, new(big.Int).SetUint64(totalGasUsed)), + TotalEth: totalEth, + EthSentToCoinbase: ethSentToCoinbase, + TotalGasUsed: totalGasUsed, + OriginalBundle: bundle, + }, nil +} + // copyReceipts makes a deep copy of the given receipts. func copyReceipts(receipts []*types.Receipt) []*types.Receipt { result := make([]*types.Receipt, len(receipts)) @@ -1945,16 +2411,74 @@ func (w *worker) postSideBlock(event core.ChainSideEvent) { } } -// totalFees computes total consumed miner fees in Wei. Block transactions and receipts have to have the same order. -func totalFees(block *types.Block, receipts []*types.Receipt) *big.Int { - feesWei := new(big.Int) +// ethIntToFloat is for formatting a big.Int in wei to eth +func ethIntToFloat(eth *big.Int) *big.Float { + if eth == nil { + return big.NewFloat(0) + } + return new(big.Float).Quo(new(big.Float).SetInt(eth), new(big.Float).SetInt(big.NewInt(params.Ether))) +} + +// totalFees computes total consumed miner fees in ETH. Block transactions and receipts have to have the same order. +func totalFees(block *types.Block, env *environment) *big.Int { + return new(big.Int).Set(env.profit) +} - for i, tx := range block.Transactions() { - minerFee, _ := tx.EffectiveGasTip(block.BaseFee()) - feesWei.Add(feesWei, new(big.Int).Mul(new(big.Int).SetUint64(receipts[i].GasUsed), minerFee)) +type proposerTxReservation struct { + builderBalance *big.Int + reservedGas uint64 + isEOA bool +} + +func (w *worker) proposerTxPrepare(env *environment, validatorCoinbase *common.Address) (*proposerTxReservation, error) { + if validatorCoinbase == nil || w.config.BuilderTxSigningKey == nil { + return nil, nil } - return feesWei + w.mu.Lock() + sender := w.coinbase + w.mu.Unlock() + builderBalance := env.state.GetBalance(sender) + + chainData := chainData{w.chainConfig, w.chain, w.blockList} + gas, isEOA, err := estimatePayoutTxGas(env, sender, *validatorCoinbase, w.config.BuilderTxSigningKey, chainData) + if err != nil { + return nil, fmt.Errorf("failed to estimate proposer payout gas: %w", err) + } + + if err := env.gasPool.SubGas(gas); err != nil { + return nil, err + } + + return &proposerTxReservation{ + builderBalance: builderBalance, + reservedGas: gas, + isEOA: isEOA, + }, nil +} + +func (w *worker) proposerTxCommit(env *environment, validatorCoinbase *common.Address, reserve *proposerTxReservation) error { + if reserve == nil || validatorCoinbase == nil { + return nil + } + + w.mu.Lock() + sender := w.coinbase + w.mu.Unlock() + builderBalance := env.state.GetBalance(sender) + + availableFunds := new(big.Int).Sub(builderBalance, reserve.builderBalance) + if availableFunds.Sign() <= 0 { + return errors.New("builder balance decreased") + } + + env.gasPool.AddGas(reserve.reservedGas) + chainData := chainData{w.chainConfig, w.chain, w.blockList} + _, err := insertPayoutTx(env, sender, *validatorCoinbase, reserve.reservedGas, reserve.isEOA, availableFunds, w.config.BuilderTxSigningKey, chainData) + if err != nil { + return err + } + return nil } // signalToErr converts the interruption signal to a concrete error type for return. @@ -1971,3 +2495,67 @@ func signalToErr(signal int32) error { panic(fmt.Errorf("undefined signal %d", signal)) } } + +func startProfiler(profile string, filepath string, number uint64) (func() error, error) { + var ( + buf bytes.Buffer + err error + ) + + closeFn := func() {} + + switch profile { + case "cpu": + err = pprof.StartCPUProfile(&buf) + + if err == nil { + closeFn = func() { + pprof.StopCPUProfile() + } + } + case "trace": + err = ptrace.Start(&buf) + + if err == nil { + closeFn = func() { + ptrace.Stop() + } + } + case "heap": + runtime.GC() + + err = pprof.WriteHeapProfile(&buf) + default: + log.Info("Incorrect profile name") + } + + if err != nil { + return func() error { + closeFn() + return nil + }, err + } + + closeFnNew := func() error { + var err error + + closeFn() + + if buf.Len() == 0 { + return nil + } + + f, err := os.Create(filepath + "/" + profile + "-" + fmt.Sprint(number) + ".prof") + if err != nil { + return err + } + + defer f.Close() + + _, err = f.Write(buf.Bytes()) + + return err + } + + return closeFnNew, nil +} From a28eef4bf5ee3c9e56cefc50e9af5a69639a9254 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Nowosielski?= Date: Wed, 25 Oct 2023 08:37:23 +0200 Subject: [PATCH 2/7] Builder RPC API - compiles, no-run, no-consensus --- Makefile | 10 +- beacon/engine/types.go | 96 ++++ builder/beacon_client.go | 51 ++ builder/beacon_client_test.go | 176 ++++++ builder/builder.go | 553 ++++++++++++++++++ builder/builder_test.go | 161 ++++++ builder/clique_builder.go | 397 +++++++++++++ builder/config.go | 66 +++ builder/eth_service.go | 104 ++++ builder/eth_service_test.go | 109 ++++ builder/files/bor-post-install.sh | 9 - builder/files/bor.service | 16 - builder/files/config.toml | 186 ------ builder/files/genesis-mainnet-v1.json | 109 ---- builder/files/genesis-testnet-v4.json | 96 ---- builder/index.go | 106 ++++ builder/local_relay.go | 467 +++++++++++++++ builder/local_relay_test.go | 279 +++++++++ builder/relay.go | 229 ++++++++ builder/relay_aggregator.go | 169 ++++++ builder/relay_aggregator_test.go | 238 ++++++++ builder/relay_test.go | 128 +++++ builder/resubmit_utils.go | 94 +++ builder/resubmit_utils_test.go | 76 +++ builder/service.go | 279 +++++++++ builder/utils.go | 119 ++++ builder/validator.go | 55 ++ core/blockchain.go | 129 +++++ core/types/builder.go | 33 ++ core/utils/gas_limit.go | 29 + eth/block-validation/api.go | 294 ++++++++++ eth/block-validation/api_test.go | 784 ++++++++++++++++++++++++++ flashbotsextra/cmd/bundle_fetcher.go | 37 ++ flashbotsextra/database.go | 370 ++++++++++++ flashbotsextra/database_test.go | 220 ++++++++ flashbotsextra/database_types.go | 111 ++++ flashbotsextra/fetcher.go | 171 ++++++ go.mod | 67 ++- go.sum | 144 +++-- light/lightchain.go | 131 +++++ 40 files changed, 6421 insertions(+), 477 deletions(-) create mode 100644 builder/beacon_client.go create mode 100644 builder/beacon_client_test.go create mode 100644 builder/builder.go create mode 100644 builder/builder_test.go create mode 100644 builder/clique_builder.go create mode 100644 builder/config.go create mode 100644 builder/eth_service.go create mode 100644 builder/eth_service_test.go delete mode 100644 builder/files/bor-post-install.sh delete mode 100644 builder/files/bor.service delete mode 100644 builder/files/config.toml delete mode 100644 builder/files/genesis-mainnet-v1.json delete mode 100644 builder/files/genesis-testnet-v4.json create mode 100644 builder/index.go create mode 100644 builder/local_relay.go create mode 100644 builder/local_relay_test.go create mode 100644 builder/relay.go create mode 100644 builder/relay_aggregator.go create mode 100644 builder/relay_aggregator_test.go create mode 100644 builder/relay_test.go create mode 100644 builder/resubmit_utils.go create mode 100644 builder/resubmit_utils_test.go create mode 100644 builder/service.go create mode 100644 builder/utils.go create mode 100644 builder/validator.go create mode 100644 core/types/builder.go create mode 100644 core/utils/gas_limit.go create mode 100644 eth/block-validation/api.go create mode 100644 eth/block-validation/api_test.go create mode 100644 flashbotsextra/cmd/bundle_fetcher.go create mode 100644 flashbotsextra/database.go create mode 100644 flashbotsextra/database_test.go create mode 100644 flashbotsextra/database_types.go create mode 100644 flashbotsextra/fetcher.go diff --git a/Makefile b/Makefile index c14d0dc9f8..717b12e110 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ # with Go source code. If you know what GOPATH is then you probably # don't need to bother with make. -.PHONY: geth android ios geth-cross evm all test clean docs +.PHONY: geth android ios geth-cross evm all test clean docs builder .PHONY: geth-linux geth-linux-386 geth-linux-amd64 geth-linux-mips64 geth-linux-mips64le .PHONY: geth-linux-arm geth-linux-arm-5 geth-linux-arm-6 geth-linux-arm-7 geth-linux-arm64 .PHONY: geth-darwin geth-darwin-386 geth-darwin-amd64 @@ -26,7 +26,13 @@ GOTEST = GODEBUG=cgocheck=0 go test $(GO_FLAGS) $(GO_LDFLAGS) -p 1 bor: mkdir -p $(GOPATH)/bin/ go build -o $(GOBIN)/bor $(GO_LDFLAGS) ./cmd/cli/main.go - cp $(GOBIN)/bor $(GOPATH)/bin/ + #cp $(GOBIN)/bor $(GOPATH)/bin/ + @echo "Done building." + +builder: + mkdir -p $(GOPATH)/bin/ + go build -o $(GOBIN)/bbuilder $(GO_LDFLAGS) ./cmd/cli/main.go + #cp $(GOBIN)/bor $(GOPATH)/bin/ @echo "Done building." protoc: diff --git a/beacon/engine/types.go b/beacon/engine/types.go index 7e3f2577fe..ef6568638d 100644 --- a/beacon/engine/types.go +++ b/beacon/engine/types.go @@ -18,6 +18,8 @@ package engine import ( "fmt" + "github.com/attestantio/go-eth2-client/spec/bellatrix" + "github.com/attestantio/go-eth2-client/spec/capella" "math/big" "github.com/ethereum/go-ethereum/common" @@ -35,6 +37,8 @@ type PayloadAttributes struct { Random common.Hash `json:"prevRandao" gencodec:"required"` SuggestedFeeRecipient common.Address `json:"suggestedFeeRecipient" gencodec:"required"` Withdrawals []*types.Withdrawal `json:"withdrawals"` + GasLimit uint64 + Slot uint64 } // JSON type overrides for PayloadAttributes. @@ -247,3 +251,95 @@ type ExecutionPayloadBodyV1 struct { TransactionData []hexutil.Bytes `json:"transactions"` Withdrawals []*types.Withdrawal `json:"withdrawals"` } + +func ExecutionPayloadToBlock(payload *bellatrix.ExecutionPayload) (*types.Block, error) { + // TODO: consolidate this into one function that handles all forks + transactionBytes := make([][]byte, len(payload.Transactions)) + for i, txHexBytes := range payload.Transactions { + transactionBytes[i] = txHexBytes[:] + } + txs, err := decodeTransactions(transactionBytes) + if err != nil { + return nil, err + } + + // base fee per gas is stored little-endian but we need it + // big-endian for big.Int. + var baseFeePerGasBytes [32]byte + for i := 0; i < 32; i++ { + baseFeePerGasBytes[i] = payload.BaseFeePerGas[32-1-i] + } + baseFeePerGas := new(big.Int).SetBytes(baseFeePerGasBytes[:]) + + header := &types.Header{ + ParentHash: common.Hash(payload.ParentHash), + UncleHash: types.EmptyUncleHash, + Coinbase: common.Address(payload.FeeRecipient), + Root: common.Hash(payload.StateRoot), + TxHash: types.DeriveSha(types.Transactions(txs), trie.NewStackTrie(nil)), + ReceiptHash: common.Hash(payload.ReceiptsRoot), + Bloom: types.BytesToBloom(payload.LogsBloom[:]), + Difficulty: common.Big0, + Number: new(big.Int).SetUint64(payload.BlockNumber), + GasLimit: payload.GasLimit, + GasUsed: payload.GasUsed, + Time: payload.Timestamp, + BaseFee: baseFeePerGas, + Extra: payload.ExtraData, + MixDigest: common.Hash(payload.PrevRandao), + } + block := types.NewBlockWithHeader(header).WithBody(txs, nil /* uncles */) + return block, nil +} + +func ExecutionPayloadV2ToBlock(payload *capella.ExecutionPayload) (*types.Block, error) { + // TODO: separate decode function to avoid allocating twice + transactionBytes := make([][]byte, len(payload.Transactions)) + for i, txHexBytes := range payload.Transactions { + transactionBytes[i] = txHexBytes[:] + } + txs, err := decodeTransactions(transactionBytes) + if err != nil { + return nil, err + } + + withdrawals := make([]*types.Withdrawal, len(payload.Withdrawals)) + for i, withdrawal := range payload.Withdrawals { + withdrawals[i] = &types.Withdrawal{ + Index: uint64(withdrawal.Index), + Validator: uint64(withdrawal.ValidatorIndex), + Address: common.Address(withdrawal.Address), + Amount: uint64(withdrawal.Amount), + } + } + withdrawalsHash := types.DeriveSha(types.Withdrawals(withdrawals), trie.NewStackTrie(nil)) + + // base fee per gas is stored little-endian but we need it + // big-endian for big.Int. + var baseFeePerGasBytes [32]byte + for i := 0; i < 32; i++ { + baseFeePerGasBytes[i] = payload.BaseFeePerGas[32-1-i] + } + baseFeePerGas := new(big.Int).SetBytes(baseFeePerGasBytes[:]) + + header := &types.Header{ + ParentHash: common.Hash(payload.ParentHash), + UncleHash: types.EmptyUncleHash, + Coinbase: common.Address(payload.FeeRecipient), + Root: common.Hash(payload.StateRoot), + TxHash: types.DeriveSha(types.Transactions(txs), trie.NewStackTrie(nil)), + ReceiptHash: common.Hash(payload.ReceiptsRoot), + Bloom: types.BytesToBloom(payload.LogsBloom[:]), + Difficulty: common.Big0, + Number: new(big.Int).SetUint64(payload.BlockNumber), + GasLimit: payload.GasLimit, + GasUsed: payload.GasUsed, + Time: payload.Timestamp, + BaseFee: baseFeePerGas, + Extra: payload.ExtraData, + MixDigest: common.Hash(payload.PrevRandao), + WithdrawalsHash: &withdrawalsHash, + } + block := types.NewBlockWithHeader(header).WithBody(txs, nil /* uncles */).WithWithdrawals(withdrawals) + return block, nil +} diff --git a/builder/beacon_client.go b/builder/beacon_client.go new file mode 100644 index 0000000000..797c376bfe --- /dev/null +++ b/builder/beacon_client.go @@ -0,0 +1,51 @@ +package builder + +import ( + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/types" +) + +type IBeaconClient interface { + isValidator(pubkey PubkeyHex) bool + getProposerForNextSlot(requestedSlot uint64) (PubkeyHex, error) + SubscribeToPayloadAttributesEvents(payloadAttrC chan types.BuilderPayloadAttributes) + Start() error + Stop() +} + +type testBeaconClient struct { + validator *ValidatorPrivateData + slot uint64 +} + +func (b *testBeaconClient) Stop() {} + +func (b *testBeaconClient) isValidator(pubkey PubkeyHex) bool { + return true +} + +func (b *testBeaconClient) getProposerForNextSlot(requestedSlot uint64) (PubkeyHex, error) { + return PubkeyHex(hexutil.Encode(b.validator.Pk)), nil +} + +func (b *testBeaconClient) SubscribeToPayloadAttributesEvents(payloadAttrC chan types.BuilderPayloadAttributes) { +} + +func (b *testBeaconClient) Start() error { return nil } + +type NilBeaconClient struct{} + +func (b *NilBeaconClient) isValidator(pubkey PubkeyHex) bool { + return false +} + +func (b *NilBeaconClient) getProposerForNextSlot(requestedSlot uint64) (PubkeyHex, error) { + return PubkeyHex(""), nil +} + +func (b *NilBeaconClient) SubscribeToPayloadAttributesEvents(payloadAttrC chan types.BuilderPayloadAttributes) { +} + +func (b *NilBeaconClient) Start() error { return nil } + +func (b *NilBeaconClient) Stop() {} diff --git a/builder/beacon_client_test.go b/builder/beacon_client_test.go new file mode 100644 index 0000000000..6073488c30 --- /dev/null +++ b/builder/beacon_client_test.go @@ -0,0 +1,176 @@ +package builder + +import ( + "net/http" + "net/http/httptest" + "strconv" + "testing" + + "github.com/gorilla/mux" + "github.com/stretchr/testify/require" +) + +type mockBeaconNode struct { + srv *httptest.Server + + proposerDuties map[int][]byte + forkResp map[int][]byte + headersCode int + headersResp []byte +} + +func newMockBeaconNode() *mockBeaconNode { + r := mux.NewRouter() + srv := httptest.NewServer(r) + + mbn := &mockBeaconNode{ + srv: srv, + + proposerDuties: make(map[int][]byte), + forkResp: make(map[int][]byte), + headersCode: 200, + headersResp: []byte{}, + } + + r.HandleFunc("/eth/v1/validator/duties/proposer/{epoch}", func(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + epochStr, ok := vars["epoch"] + if !ok { + http.Error(w, `{ "code": 400, "message": "Invalid epoch" }`, 400) + return + } + epoch, err := strconv.Atoi(epochStr) + if err != nil { + http.Error(w, `{ "code": 400, "message": "Invalid epoch" }`, 400) + return + } + + resp, found := mbn.proposerDuties[epoch] + if !found { + http.Error(w, `{ "code": 400, "message": "Invalid epoch" }`, 400) + return + } + + w.Header().Set("Content-Type", "application/json") + w.Write(resp) + }) + + r.HandleFunc("/eth/v1/beacon/headers", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(mbn.headersCode) + w.Write(mbn.headersResp) + }) + + return mbn +} + +func TestFetchBeacon(t *testing.T) { + mbn := newMockBeaconNode() + defer mbn.srv.Close() + + mbn.headersCode = 200 + mbn.headersResp = []byte(`{ "data": [ { "header": { "message": { "slot": "10", "proposer_index": "1" } } } ] }`) + + // Green path + headersResp := struct { + Data []struct { + Header struct { + Message struct { + Slot string `json:"slot"` + } `json:"message"` + } `json:"header"` + } `json:"data"` + }{} + err := fetchBeacon(mbn.srv.URL+"/eth/v1/beacon/headers", &headersResp) + require.NoError(t, err) + require.Equal(t, "10", headersResp.Data[0].Header.Message.Slot) + + // Wrong dst + wrongForkResp := struct { + Data []struct { + Slot string `json:"slot"` + } + }{} + err = fetchBeacon(mbn.srv.URL+"/eth/v1/beacon/headers", &wrongForkResp) + require.NoError(t, err) + require.Equal(t, wrongForkResp.Data[0].Slot, "") + + mbn.headersCode = 400 + mbn.headersResp = []byte(`{ "code": 400, "message": "Invalid call" }`) + err = fetchBeacon(mbn.srv.URL+"/eth/v1/beacon/headers", &headersResp) + require.EqualError(t, err, "Invalid call") +} + +func TestFetchCurrentSlot(t *testing.T) { + mbn := newMockBeaconNode() + defer mbn.srv.Close() + + mbn.headersResp = []byte(`{ + "execution_optimistic": false, + "data": [ + { + "root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", + "canonical": true, + "header": { + "message": { + "slot": "101", + "proposer_index": "1", + "parent_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", + "state_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", + "body_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2" + }, + "signature": "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505" + } + } + ] +}`) + + slot, err := fetchCurrentSlot(mbn.srv.URL) + require.NoError(t, err) + require.Equal(t, uint64(101), slot) + + mbn.headersResp = []byte(`{ + "execution_optimistic": false, + "data": [ + { + "header": { + "message": { + "slot": "xxx" + } + } + } + ] +}`) + + slot, err = fetchCurrentSlot(mbn.srv.URL) + require.EqualError(t, err, "invalid response") + require.Equal(t, uint64(0), slot) +} + +func TestFetchEpochProposersMap(t *testing.T) { + mbn := newMockBeaconNode() + defer mbn.srv.Close() + + mbn.proposerDuties[10] = []byte(`{ + "dependent_root": "0xcf8e0d4e9587369b2301d0790347320302cc0943d5a1884560367e8208d920f2", + "execution_optimistic": false, + "data": [ + { + "pubkey": "0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a", + "validator_index": "1", + "slot": "1" + }, + { + "pubkey": "0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74b", + "validator_index": "2", + "slot": "2" + } + ] +}`) + + proposersMap, err := fetchEpochProposersMap(mbn.srv.URL, 10) + require.NoError(t, err) + require.Equal(t, 2, len(proposersMap)) + require.Equal(t, PubkeyHex("0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a"), proposersMap[1]) + require.Equal(t, PubkeyHex("0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74b"), proposersMap[2]) +} diff --git a/builder/builder.go b/builder/builder.go new file mode 100644 index 0000000000..44201bcdeb --- /dev/null +++ b/builder/builder.go @@ -0,0 +1,553 @@ +package builder + +import ( + "context" + "errors" + "fmt" + "math/big" + _ "os" + "sync" + "time" + + bellatrixapi "github.com/attestantio/go-builder-client/api/bellatrix" + capellaapi "github.com/attestantio/go-builder-client/api/capella" + apiv1 "github.com/attestantio/go-builder-client/api/v1" + "github.com/attestantio/go-eth2-client/spec/bellatrix" + "github.com/attestantio/go-eth2-client/spec/capella" + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/ethereum/go-ethereum/beacon/engine" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + blockvalidation "github.com/ethereum/go-ethereum/eth/block-validation" + "github.com/ethereum/go-ethereum/flashbotsextra" + "github.com/ethereum/go-ethereum/log" + "github.com/flashbots/go-boost-utils/bls" + "github.com/flashbots/go-boost-utils/ssz" + boostTypes "github.com/flashbots/go-boost-utils/types" + "github.com/flashbots/go-boost-utils/utils" + "github.com/holiman/uint256" + "golang.org/x/time/rate" +) + +const ( + RateLimitIntervalDefault = 500 * time.Millisecond + RateLimitBurstDefault = 10 + BlockResubmitIntervalDefault = 500 * time.Millisecond + + SubmissionOffsetFromEndOfSlotSecondsDefault = 3 * time.Second +) + +type PubkeyHex string + +type ValidatorData struct { + Pubkey PubkeyHex + FeeRecipient bellatrix.ExecutionAddress + GasLimit uint64 +} + +type IRelay interface { + SubmitBlock(msg *bellatrixapi.SubmitBlockRequest, vd ValidatorData) error + SubmitBlockCapella(msg *capellaapi.SubmitBlockRequest, vd ValidatorData) error + GetValidatorForSlot(nextSlot uint64) (ValidatorData, error) + Config() RelayConfig + Start() error + Stop() +} + +type IBuilder interface { + OnPayloadAttribute(attrs *types.BuilderPayloadAttributes) error + Start() error + Stop() error +} + +type Builder struct { + ds flashbotsextra.IDatabaseService + relay IRelay + eth IEthereumService + dryRun bool + ignoreLatePayloadAttributes bool + validator *blockvalidation.BlockValidationAPI + beaconClient IBeaconClient + builderSecretKey *bls.SecretKey + builderPublicKey phase0.BLSPubKey + builderSigningDomain phase0.Domain + builderResubmitInterval time.Duration + discardRevertibleTxOnErr bool + + limiter *rate.Limiter + submissionOffsetFromEndOfSlot time.Duration + + slotMu sync.Mutex + slotAttrs types.BuilderPayloadAttributes + slotCtx context.Context + slotCtxCancel context.CancelFunc + + stop chan struct{} +} + +// BuilderArgs is a struct that contains all the arguments needed to create a new Builder +type BuilderArgs struct { + sk *bls.SecretKey + ds flashbotsextra.IDatabaseService + relay IRelay + builderSigningDomain phase0.Domain + builderBlockResubmitInterval time.Duration + discardRevertibleTxOnErr bool + eth IEthereumService + dryRun bool + ignoreLatePayloadAttributes bool + validator *blockvalidation.BlockValidationAPI + beaconClient IBeaconClient + submissionOffsetFromEndOfSlot time.Duration + + limiter *rate.Limiter +} + +func NewBuilder(args BuilderArgs) (*Builder, error) { + blsPk, err := bls.PublicKeyFromSecretKey(args.sk) + if err != nil { + return nil, err + } + pk, err := utils.BlsPublicKeyToPublicKey(blsPk) + if err != nil { + return nil, err + } + + if args.limiter == nil { + args.limiter = rate.NewLimiter(rate.Every(RateLimitIntervalDefault), RateLimitBurstDefault) + } + + if args.builderBlockResubmitInterval == 0 { + args.builderBlockResubmitInterval = BlockResubmitIntervalDefault + } + + if args.submissionOffsetFromEndOfSlot == 0 { + args.submissionOffsetFromEndOfSlot = SubmissionOffsetFromEndOfSlotSecondsDefault + } + + slotCtx, slotCtxCancel := context.WithCancel(context.Background()) + return &Builder{ + ds: args.ds, + relay: args.relay, + eth: args.eth, + dryRun: args.dryRun, + ignoreLatePayloadAttributes: args.ignoreLatePayloadAttributes, + validator: args.validator, + beaconClient: args.beaconClient, + builderSecretKey: args.sk, + builderPublicKey: pk, + builderSigningDomain: args.builderSigningDomain, + builderResubmitInterval: args.builderBlockResubmitInterval, + discardRevertibleTxOnErr: args.discardRevertibleTxOnErr, + submissionOffsetFromEndOfSlot: args.submissionOffsetFromEndOfSlot, + + limiter: args.limiter, + slotCtx: slotCtx, + slotCtxCancel: slotCtxCancel, + + stop: make(chan struct{}, 1), + }, nil +} + +func (b *Builder) Start() error { + // Start regular payload attributes updates + go func() { + c := make(chan types.BuilderPayloadAttributes) + go b.beaconClient.SubscribeToPayloadAttributesEvents(c) + + currentSlot := uint64(0) + + for { + select { + case <-b.stop: + return + case payloadAttributes := <-c: + // Right now we are building only on a single head. This might change in the future! + if payloadAttributes.Slot < currentSlot { + continue + } else if payloadAttributes.Slot == currentSlot { + // Subsequent sse events should only be canonical! + if !b.ignoreLatePayloadAttributes { + err := b.OnPayloadAttribute(&payloadAttributes) + if err != nil { + log.Error("error with builder processing on payload attribute", + "latestSlot", currentSlot, + "processedSlot", payloadAttributes.Slot, + "headHash", payloadAttributes.HeadHash.String(), + "error", err) + } + } + } else if payloadAttributes.Slot > currentSlot { + currentSlot = payloadAttributes.Slot + err := b.OnPayloadAttribute(&payloadAttributes) + if err != nil { + log.Error("error with builder processing on payload attribute", + "latestSlot", currentSlot, + "processedSlot", payloadAttributes.Slot, + "headHash", payloadAttributes.HeadHash.String(), + "error", err) + } + } + } + } + }() + + return b.relay.Start() +} + +func (b *Builder) Stop() error { + close(b.stop) + return nil +} + +func (b *Builder) onSealedBlock(block *types.Block, blockValue *big.Int, ordersClosedAt, sealedAt time.Time, + commitedBundles, allBundles []types.SimulatedBundle, usedSbundles []types.UsedSBundle, + proposerPubkey phase0.BLSPubKey, vd ValidatorData, attrs *types.BuilderPayloadAttributes) error { + if b.eth.Config().IsShanghai(block.Time()) { + if err := b.submitCapellaBlock(block, blockValue, ordersClosedAt, sealedAt, commitedBundles, allBundles, usedSbundles, proposerPubkey, vd, attrs); err != nil { + return err + } + } else { + if err := b.submitBellatrixBlock(block, blockValue, ordersClosedAt, sealedAt, commitedBundles, allBundles, usedSbundles, proposerPubkey, vd, attrs); err != nil { + return err + } + } + + log.Info("submitted block", "slot", attrs.Slot, "value", blockValue.String(), "parent", block.ParentHash, + "hash", block.Hash(), "#commitedBundles", len(commitedBundles)) + + return nil +} + +func (b *Builder) submitBellatrixBlock(block *types.Block, blockValue *big.Int, ordersClosedAt, sealedAt time.Time, + commitedBundles, allBundles []types.SimulatedBundle, usedSbundles []types.UsedSBundle, + proposerPubkey phase0.BLSPubKey, vd ValidatorData, attrs *types.BuilderPayloadAttributes) error { + executableData := engine.BlockToExecutableData(block, blockValue) + payload, err := executableDataToExecutionPayload(executableData.ExecutionPayload) + if err != nil { + log.Error("could not format execution payload", "err", err) + return err + } + + value, overflow := uint256.FromBig(blockValue) + if overflow { + log.Error("could not set block value due to value overflow") + return err + } + + blockBidMsg := apiv1.BidTrace{ + Slot: attrs.Slot, + ParentHash: payload.ParentHash, + BlockHash: payload.BlockHash, + BuilderPubkey: b.builderPublicKey, + ProposerPubkey: proposerPubkey, + ProposerFeeRecipient: vd.FeeRecipient, + GasLimit: executableData.ExecutionPayload.GasLimit, + GasUsed: executableData.ExecutionPayload.GasUsed, + Value: value, + } + + signature, err := ssz.SignMessage(&blockBidMsg, b.builderSigningDomain, b.builderSecretKey) + if err != nil { + log.Error("could not sign builder bid", "err", err) + return err + } + + blockSubmitReq := bellatrixapi.SubmitBlockRequest{ + Signature: signature, + Message: &blockBidMsg, + ExecutionPayload: payload, + } + + if b.dryRun { + err = b.validator.ValidateBuilderSubmissionV1(&blockvalidation.BuilderBlockValidationRequest{SubmitBlockRequest: blockSubmitReq, RegisteredGasLimit: vd.GasLimit}) + if err != nil { + log.Error("could not validate bellatrix block", "err", err) + } + } else { + go b.ds.ConsumeBuiltBlock(block, blockValue, ordersClosedAt, sealedAt, commitedBundles, allBundles, usedSbundles, &blockBidMsg) + err = b.relay.SubmitBlock(&blockSubmitReq, vd) + if err != nil { + log.Error("could not submit bellatrix block", "err", err, "#commitedBundles", len(commitedBundles)) + return err + } + } + + log.Info("submitted bellatrix block", "slot", blockBidMsg.Slot, "value", blockBidMsg.Value.String(), "parent", blockBidMsg.ParentHash, "hash", block.Hash(), "#commitedBundles", len(commitedBundles)) + + return nil +} + +func (b *Builder) submitCapellaBlock(block *types.Block, blockValue *big.Int, ordersClosedAt, sealedAt time.Time, + commitedBundles, allBundles []types.SimulatedBundle, usedSbundles []types.UsedSBundle, + proposerPubkey phase0.BLSPubKey, vd ValidatorData, attrs *types.BuilderPayloadAttributes) error { + executableData := engine.BlockToExecutableData(block, blockValue) + payload, err := executableDataToCapellaExecutionPayload(executableData.ExecutionPayload) + if err != nil { + log.Error("could not format execution payload", "err", err) + return err + } + + value, overflow := uint256.FromBig(blockValue) + if overflow { + log.Error("could not set block value due to value overflow") + return err + } + + blockBidMsg := apiv1.BidTrace{ + Slot: attrs.Slot, + ParentHash: payload.ParentHash, + BlockHash: payload.BlockHash, + BuilderPubkey: b.builderPublicKey, + ProposerPubkey: proposerPubkey, + ProposerFeeRecipient: vd.FeeRecipient, + GasLimit: executableData.ExecutionPayload.GasLimit, + GasUsed: executableData.ExecutionPayload.GasUsed, + Value: value, + } + + signature, err := ssz.SignMessage(&blockBidMsg, b.builderSigningDomain, b.builderSecretKey) + if err != nil { + log.Error("could not sign builder bid", "err", err) + return err + } + + blockSubmitReq := capellaapi.SubmitBlockRequest{ + Signature: signature, + Message: &blockBidMsg, + ExecutionPayload: payload, + } + + if b.dryRun { + err = b.validator.ValidateBuilderSubmissionV2(&blockvalidation.BuilderBlockValidationRequestV2{SubmitBlockRequest: blockSubmitReq, RegisteredGasLimit: vd.GasLimit}) + if err != nil { + log.Error("could not validate block for capella", "err", err) + } + } else { + go b.ds.ConsumeBuiltBlock(block, blockValue, ordersClosedAt, sealedAt, commitedBundles, allBundles, usedSbundles, &blockBidMsg) + err = b.relay.SubmitBlockCapella(&blockSubmitReq, vd) + if err != nil { + log.Error("could not submit capella block", "err", err, "#commitedBundles", len(commitedBundles)) + return err + } + } + + log.Info("submitted capella block", "slot", blockBidMsg.Slot, "value", blockBidMsg.Value.String(), "parent", blockBidMsg.ParentHash, "hash", block.Hash(), "#commitedBundles", len(commitedBundles)) + return nil +} + +func (b *Builder) OnPayloadAttribute(attrs *types.BuilderPayloadAttributes) error { + if attrs == nil { + return nil + } + + vd, err := b.relay.GetValidatorForSlot(attrs.Slot) + if err != nil { + return fmt.Errorf("could not get validator while submitting block for slot %d - %w", attrs.Slot, err) + } + + attrs.SuggestedFeeRecipient = [20]byte(vd.FeeRecipient) + attrs.GasLimit = vd.GasLimit + + proposerPubkey, err := utils.HexToPubkey(string(vd.Pubkey)) + if err != nil { + return fmt.Errorf("could not parse pubkey (%s) - %w", vd.Pubkey, err) + } + + if !b.eth.Synced() { + return errors.New("backend not Synced") + } + + parentBlock := b.eth.GetBlockByHash(attrs.HeadHash) + if parentBlock == nil { + return fmt.Errorf("parent block hash not found in block tree given head block hash %s", attrs.HeadHash) + } + + b.slotMu.Lock() + defer b.slotMu.Unlock() + + if attrs.Equal(&b.slotAttrs) { + log.Debug("ignoring known payload attribute", "slot", attrs.Slot, "hash", attrs.HeadHash) + return nil + } + + if b.slotCtxCancel != nil { + b.slotCtxCancel() + } + + slotCtx, slotCtxCancel := context.WithTimeout(context.Background(), 12*time.Second) + b.slotAttrs = *attrs + b.slotCtx = slotCtx + b.slotCtxCancel = slotCtxCancel + + go b.runBuildingJob(b.slotCtx, proposerPubkey, vd, attrs) + return nil +} + +type blockQueueEntry struct { + block *types.Block + blockValue *big.Int + ordersCloseTime time.Time + sealedAt time.Time + commitedBundles []types.SimulatedBundle + allBundles []types.SimulatedBundle + usedSbundles []types.UsedSBundle +} + +func (b *Builder) runBuildingJob(slotCtx context.Context, proposerPubkey phase0.BLSPubKey, vd ValidatorData, attrs *types.BuilderPayloadAttributes) { + ctx, cancel := context.WithTimeout(slotCtx, 12*time.Second) + defer cancel() + + // Submission queue for the given payload attributes + // multiple jobs can run for different attributes fot the given slot + // 1. When new block is ready we check if its profit is higher than profit of last best block + // if it is we set queueBest* to values of the new block and notify queueSignal channel. + // 2. Submission goroutine waits for queueSignal and submits queueBest* if its more valuable than + // queueLastSubmittedProfit keeping queueLastSubmittedProfit to be the profit of the last submission. + // Submission goroutine is globally rate limited to have fixed rate of submissions for all jobs. + var ( + queueSignal = make(chan struct{}, 1) + + queueMu sync.Mutex + queueLastSubmittedHash common.Hash + queueBestEntry blockQueueEntry + ) + + log.Debug("runBuildingJob", "slot", attrs.Slot, "parent", attrs.HeadHash, "payloadTimestamp", uint64(attrs.Timestamp)) + + submitBestBlock := func() { + queueMu.Lock() + if queueBestEntry.block.Hash() != queueLastSubmittedHash { + err := b.onSealedBlock(queueBestEntry.block, queueBestEntry.blockValue, queueBestEntry.ordersCloseTime, queueBestEntry.sealedAt, + queueBestEntry.commitedBundles, queueBestEntry.allBundles, queueBestEntry.usedSbundles, proposerPubkey, vd, attrs) + + if err != nil { + log.Error("could not run sealed block hook", "err", err) + } else { + queueLastSubmittedHash = queueBestEntry.block.Hash() + } + } + queueMu.Unlock() + } + + // Avoid submitting early into a given slot. For example if slots have 12 second interval, submissions should + // not begin until 8 seconds into the slot. + slotTime := time.Unix(int64(attrs.Timestamp), 0).UTC() + slotSubmitStartTime := slotTime.Add(-b.submissionOffsetFromEndOfSlot) + + // Empties queue, submits the best block for current job with rate limit (global for all jobs) + go runResubmitLoop(ctx, b.limiter, queueSignal, submitBestBlock, slotSubmitStartTime) + + // Populates queue with submissions that increase block profit + blockHook := func(block *types.Block, blockValue *big.Int, ordersCloseTime time.Time, + committedBundles, allBundles []types.SimulatedBundle, usedSbundles []types.UsedSBundle, + ) { + if ctx.Err() != nil { + return + } + + sealedAt := time.Now() + + queueMu.Lock() + defer queueMu.Unlock() + if block.Hash() != queueLastSubmittedHash { + queueBestEntry = blockQueueEntry{ + block: block, + blockValue: new(big.Int).Set(blockValue), + ordersCloseTime: ordersCloseTime, + sealedAt: sealedAt, + commitedBundles: committedBundles, + allBundles: allBundles, + usedSbundles: usedSbundles, + } + + select { + case queueSignal <- struct{}{}: + default: + } + } + } + + // resubmits block builder requests every builderBlockResubmitInterval + runRetryLoop(ctx, b.builderResubmitInterval, func() { + log.Debug("retrying BuildBlock", + "slot", attrs.Slot, + "parent", attrs.HeadHash, + "resubmit-interval", b.builderResubmitInterval.String()) + err := b.eth.BuildBlock(attrs, blockHook) + if err != nil { + log.Warn("Failed to build block", "err", err) + } + }) +} + +func executableDataToExecutionPayload(data *engine.ExecutableData) (*bellatrix.ExecutionPayload, error) { + transactionData := make([]bellatrix.Transaction, len(data.Transactions)) + for i, tx := range data.Transactions { + transactionData[i] = bellatrix.Transaction(tx) + } + + baseFeePerGas := new(boostTypes.U256Str) + err := baseFeePerGas.FromBig(data.BaseFeePerGas) + if err != nil { + return nil, err + } + + return &bellatrix.ExecutionPayload{ + ParentHash: [32]byte(data.ParentHash), + FeeRecipient: [20]byte(data.FeeRecipient), + StateRoot: [32]byte(data.StateRoot), + ReceiptsRoot: [32]byte(data.ReceiptsRoot), + LogsBloom: types.BytesToBloom(data.LogsBloom), + PrevRandao: [32]byte(data.Random), + BlockNumber: data.Number, + GasLimit: data.GasLimit, + GasUsed: data.GasUsed, + Timestamp: data.Timestamp, + ExtraData: data.ExtraData, + BaseFeePerGas: *baseFeePerGas, + BlockHash: [32]byte(data.BlockHash), + Transactions: transactionData, + }, nil +} + +func executableDataToCapellaExecutionPayload(data *engine.ExecutableData) (*capella.ExecutionPayload, error) { + transactionData := make([]bellatrix.Transaction, len(data.Transactions)) + for i, tx := range data.Transactions { + transactionData[i] = bellatrix.Transaction(tx) + } + + withdrawalData := make([]*capella.Withdrawal, len(data.Withdrawals)) + for i, wd := range data.Withdrawals { + withdrawalData[i] = &capella.Withdrawal{ + Index: capella.WithdrawalIndex(wd.Index), + ValidatorIndex: phase0.ValidatorIndex(wd.Validator), + Address: bellatrix.ExecutionAddress(wd.Address), + Amount: phase0.Gwei(wd.Amount), + } + } + + baseFeePerGas := new(boostTypes.U256Str) + err := baseFeePerGas.FromBig(data.BaseFeePerGas) + if err != nil { + return nil, err + } + + return &capella.ExecutionPayload{ + ParentHash: [32]byte(data.ParentHash), + FeeRecipient: [20]byte(data.FeeRecipient), + StateRoot: [32]byte(data.StateRoot), + ReceiptsRoot: [32]byte(data.ReceiptsRoot), + LogsBloom: types.BytesToBloom(data.LogsBloom), + PrevRandao: [32]byte(data.Random), + BlockNumber: data.Number, + GasLimit: data.GasLimit, + GasUsed: data.GasUsed, + Timestamp: data.Timestamp, + ExtraData: data.ExtraData, + BaseFeePerGas: *baseFeePerGas, + BlockHash: [32]byte(data.BlockHash), + Transactions: transactionData, + Withdrawals: withdrawalData, + }, nil +} diff --git a/builder/builder_test.go b/builder/builder_test.go new file mode 100644 index 0000000000..7c8c4cca9e --- /dev/null +++ b/builder/builder_test.go @@ -0,0 +1,161 @@ +package builder + +import ( + "math/big" + "testing" + "time" + + apiv1 "github.com/attestantio/go-builder-client/api/v1" + "github.com/attestantio/go-eth2-client/spec/bellatrix" + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/ethereum/go-ethereum/beacon/engine" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/flashbotsextra" + "github.com/flashbots/go-boost-utils/bls" + "github.com/flashbots/go-boost-utils/ssz" + "github.com/flashbots/go-boost-utils/utils" + "github.com/holiman/uint256" + "github.com/stretchr/testify/require" +) + +func TestOnPayloadAttributes(t *testing.T) { + vsk, err := bls.SecretKeyFromBytes(hexutil.MustDecode("0x370bb8c1a6e62b2882f6ec76762a67b39609002076b95aae5b023997cf9b2dc9")) + require.NoError(t, err) + validator := &ValidatorPrivateData{ + sk: vsk, + Pk: hexutil.MustDecode("0xb67d2c11bcab8c4394fc2faa9601d0b99c7f4b37e14911101da7d97077917862eed4563203d34b91b5cf0aa44d6cfa05"), + } + + testBeacon := testBeaconClient{ + validator: validator, + slot: 56, + } + + feeRecipient, _ := utils.HexToAddress("0xabcf8e0d4e9587369b2301d0790347320302cc00") + testRelay := testRelay{ + gvsVd: ValidatorData{ + Pubkey: PubkeyHex(testBeacon.validator.Pk.String()), + FeeRecipient: feeRecipient, + GasLimit: 10, + }, + } + + sk, err := bls.SecretKeyFromBytes(hexutil.MustDecode("0x31ee185dad1220a8c88ca5275e64cf5a5cb09cb621cb30df52c9bee8fbaaf8d7")) + require.NoError(t, err) + + bDomain := ssz.ComputeDomain(ssz.DomainTypeAppBuilder, [4]byte{0x02, 0x0, 0x0, 0x0}, phase0.Root{}) + + testExecutableData := &engine.ExecutableData{ + ParentHash: common.Hash{0x02, 0x03}, + FeeRecipient: common.Address(feeRecipient), + StateRoot: common.Hash{0x07, 0x16}, + ReceiptsRoot: common.Hash{0x08, 0x20}, + LogsBloom: types.Bloom{}.Bytes(), + Number: uint64(10), + GasLimit: uint64(50), + GasUsed: uint64(100), + Timestamp: uint64(105), + ExtraData: hexutil.MustDecode("0x0042fafc"), + + BaseFeePerGas: big.NewInt(16), + + BlockHash: common.HexToHash("0xca4147f0d4150183ece9155068f34ee3c375448814e4ca557d482b1d40ee5407"), + Transactions: [][]byte{}, + } + + testBlock, err := engine.ExecutableDataToBlock(*testExecutableData) + require.NoError(t, err) + + testPayloadAttributes := &types.BuilderPayloadAttributes{ + Timestamp: hexutil.Uint64(104), + Random: common.Hash{0x05, 0x10}, + SuggestedFeeRecipient: common.Address{0x04, 0x10}, + GasLimit: uint64(21), + Slot: uint64(25), + } + + testEthService := &testEthereumService{synced: true, testExecutableData: testExecutableData, testBlock: testBlock, testBlockValue: big.NewInt(10)} + builderArgs := BuilderArgs{ + sk: sk, + ds: flashbotsextra.NilDbService{}, + relay: &testRelay, + builderSigningDomain: bDomain, + eth: testEthService, + dryRun: false, + ignoreLatePayloadAttributes: false, + validator: nil, + beaconClient: &testBeacon, + limiter: nil, + } + builder, err := NewBuilder(builderArgs) + require.NoError(t, err) + builder.Start() + defer builder.Stop() + + err = builder.OnPayloadAttribute(testPayloadAttributes) + require.NoError(t, err) + time.Sleep(time.Second * 3) + + require.NotNil(t, testRelay.submittedMsg) + expectedProposerPubkey, err := utils.HexToPubkey(testBeacon.validator.Pk.String()) + require.NoError(t, err) + + expectedMessage := apiv1.BidTrace{ + Slot: uint64(25), + ParentHash: phase0.Hash32{0x02, 0x03}, + BuilderPubkey: builder.builderPublicKey, + ProposerPubkey: expectedProposerPubkey, + ProposerFeeRecipient: feeRecipient, + GasLimit: uint64(50), + GasUsed: uint64(100), + Value: &uint256.Int{0x0a}, + } + copy(expectedMessage.BlockHash[:], hexutil.MustDecode("0xca4147f0d4150183ece9155068f34ee3c375448814e4ca557d482b1d40ee5407")[:]) + require.Equal(t, expectedMessage, *testRelay.submittedMsg.Message) + + expectedExecutionPayload := bellatrix.ExecutionPayload{ + ParentHash: [32]byte(testExecutableData.ParentHash), + FeeRecipient: feeRecipient, + StateRoot: [32]byte(testExecutableData.StateRoot), + ReceiptsRoot: [32]byte(testExecutableData.ReceiptsRoot), + LogsBloom: [256]byte{}, + PrevRandao: [32]byte(testExecutableData.Random), + BlockNumber: testExecutableData.Number, + GasLimit: testExecutableData.GasLimit, + GasUsed: testExecutableData.GasUsed, + Timestamp: testExecutableData.Timestamp, + ExtraData: hexutil.MustDecode("0x0042fafc"), + BaseFeePerGas: [32]byte{0x10}, + BlockHash: expectedMessage.BlockHash, + Transactions: []bellatrix.Transaction{}, + } + + require.Equal(t, expectedExecutionPayload, *testRelay.submittedMsg.ExecutionPayload) + + expectedSignature, err := utils.HexToSignature("0xad09f171b1da05636acfc86778c319af69e39c79515d44bdfed616ba2ef677ffd4d155d87b3363c6bae651ce1e92786216b75f1ac91dd65f3b1d1902bf8485e742170732dd82ffdf4decb0151eeb7926dd053efa9794b2ebed1a203e62bb13e9") + + require.NoError(t, err) + require.Equal(t, expectedSignature, testRelay.submittedMsg.Signature) + + require.Equal(t, uint64(25), testRelay.requestedSlot) + + // Clear the submitted message and check that the job will be ran again and but a new message will not be submitted since the hash is the same + testEthService.testBlockValue = big.NewInt(10) + + testRelay.submittedMsg = nil + time.Sleep(2200 * time.Millisecond) + require.Nil(t, testRelay.submittedMsg) + + // Change the hash, expect to get the block + testExecutableData.ExtraData = hexutil.MustDecode("0x0042fafd") + testExecutableData.BlockHash = common.HexToHash("0x0579b1aaca5c079c91e5774bac72c7f9bc2ddf2b126e9c632be68a1cb8f3fc71") + testBlock, err = engine.ExecutableDataToBlock(*testExecutableData) + testEthService.testBlockValue = big.NewInt(10) + require.NoError(t, err) + testEthService.testBlock = testBlock + + time.Sleep(2200 * time.Millisecond) + require.NotNil(t, testRelay.submittedMsg) +} diff --git a/builder/clique_builder.go b/builder/clique_builder.go new file mode 100644 index 0000000000..e91d352d5c --- /dev/null +++ b/builder/clique_builder.go @@ -0,0 +1,397 @@ +package builder + +import ( + "context" + "errors" + "math/big" + "sync" + "time" + + bellatrixapi "github.com/attestantio/go-builder-client/api/bellatrix" + capellaapi "github.com/attestantio/go-builder-client/api/capella" + apiv1 "github.com/attestantio/go-builder-client/api/v1" + "github.com/attestantio/go-eth2-client/spec/bellatrix" + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/ethereum/go-ethereum/beacon/engine" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/types" + blockvalidation "github.com/ethereum/go-ethereum/eth/block-validation" + "github.com/ethereum/go-ethereum/event" + "github.com/ethereum/go-ethereum/flashbotsextra" + "github.com/ethereum/go-ethereum/log" + "github.com/flashbots/go-boost-utils/bls" + "github.com/flashbots/go-boost-utils/ssz" + "github.com/flashbots/go-boost-utils/utils" + "github.com/holiman/uint256" + "golang.org/x/time/rate" +) + +type CliqueBuilder struct { + ds flashbotsextra.IDatabaseService + relay IRelay + eth IEthereumService + blockTime time.Duration + // clique assumes centralized sequencer and therefore has only one proposer configuration. + proposerPubkey phase0.BLSPubKey + proposerFeeRecipient bellatrix.ExecutionAddress + proposerGasLimit uint64 + builderSecretKey *bls.SecretKey + builderPublicKey phase0.BLSPubKey + builderSigningDomain phase0.Domain + builderResubmitInterval time.Duration + limiter *rate.Limiter + + submissionOffsetFromEndOfSlot time.Duration + slotMu sync.Mutex + slotBlock *types.Block + slotCtx context.Context + slotCtxCancel context.CancelFunc + + chainHeadCh chan core.ChainHeadEvent + chainHeadSub event.Subscription + + stop chan struct{} +} + +type CliqueBuilderArgs struct { + sk *bls.SecretKey + ds flashbotsextra.IDatabaseService + relay IRelay + blockTime time.Duration + proposerPubkey phase0.BLSPubKey + proposerFeeRecipient bellatrix.ExecutionAddress + proposerGasLimit uint64 + builderSigningDomain phase0.Domain + builderBlockResubmitInterval time.Duration + eth IEthereumService + limiter *rate.Limiter + submissionOffsetFromEndOfSlot time.Duration + validator *blockvalidation.BlockValidationAPI +} + +func NewCliqueBuilder(args CliqueBuilderArgs) (*CliqueBuilder, error) { + blsPk, err := bls.PublicKeyFromSecretKey(args.sk) + if err != nil { + return nil, err + } + + pk, err := utils.BlsPublicKeyToPublicKey(blsPk) + if err != nil { + return nil, err + } + + if args.limiter == nil { + args.limiter = rate.NewLimiter(rate.Every(RateLimitIntervalDefault), RateLimitBurstDefault) + } + + if args.builderBlockResubmitInterval == 0 { + args.builderBlockResubmitInterval = BlockResubmitIntervalDefault + } + + if args.submissionOffsetFromEndOfSlot == 0 { + args.submissionOffsetFromEndOfSlot = SubmissionOffsetFromEndOfSlotSecondsDefault + } + + slotCtx, slotCtxCancel := context.WithCancel(context.Background()) + + return &CliqueBuilder{ + ds: args.ds, + relay: args.relay, + eth: args.eth, + proposerPubkey: args.proposerPubkey, + proposerFeeRecipient: args.proposerFeeRecipient, + proposerGasLimit: args.proposerGasLimit, + builderSecretKey: args.sk, + builderPublicKey: pk, + builderSigningDomain: args.builderSigningDomain, + builderResubmitInterval: args.builderBlockResubmitInterval, + submissionOffsetFromEndOfSlot: args.submissionOffsetFromEndOfSlot, + limiter: args.limiter, + slotCtx: slotCtx, + slotCtxCancel: slotCtxCancel, + stop: make(chan struct{}, 1), + }, nil +} + +func (cb *CliqueBuilder) Start() error { + go func() { + c := make(chan core.ChainHeadEvent) + //TODO: How to connect to Bor server? + //cb.eth.BlockChain().SubscribeChainHeadEvent(c) + for { + select { + case <-cb.stop: + return + case head := <-c: + cb.onChainHeadEvent(head.Block) + } + } + }() + + return cb.relay.Start() +} + +func (cb *CliqueBuilder) getValidatorData() ValidatorData { + return ValidatorData{ + Pubkey: PubkeyHex(cb.proposerPubkey.String()), + FeeRecipient: cb.proposerFeeRecipient, + GasLimit: cb.proposerGasLimit, + } +} + +func (cb *CliqueBuilder) onChainHeadEvent(block *types.Block) error { + if block == nil { + return nil + } + + if !cb.eth.Synced() { + return errors.New("not synced") + } + + cb.slotMu.Lock() + defer cb.slotMu.Unlock() + if cb.slotCtxCancel != nil { + cb.slotCtxCancel() + } + + slotCtx, slotCtxCancel := context.WithTimeout(context.Background(), 6*time.Second) + cb.slotBlock = block + cb.slotCtx = slotCtx + cb.slotCtxCancel = slotCtxCancel + + attrs := &types.BuilderPayloadAttributes{ + Timestamp: hexutil.Uint64(block.Header().Time), + Random: common.Hash{}, // unused + SuggestedFeeRecipient: common.Address{}, // unused + Slot: block.NumberU64() + 1, + HeadHash: block.Hash(), + Withdrawals: block.Withdrawals(), + GasLimit: block.GasLimit(), + } + + go cb.runBuildingJob(cb.slotCtx, attrs) + return nil +} + +func (cb *CliqueBuilder) runBuildingJob(slotCtx context.Context, attrs *types.BuilderPayloadAttributes) { + ctx, cancel := context.WithTimeout(slotCtx, cb.blockTime) + defer cancel() + + // Submission queue for the given payload attributes + // multiple jobs can run for different attributes fot the given slot + // 1. When new block is ready we check if its profit is higher than profit of last best block + // if it is we set queueBest* to values of the new block and notify queueSignal channel. + // 2. Submission goroutine waits for queueSignal and submits queueBest* if its more valuable than + // queueLastSubmittedProfit keeping queueLastSubmittedProfit to be the profit of the last submission. + // Submission goroutine is globally rate limited to have fixed rate of submissions for all jobs. + var ( + queueSignal = make(chan struct{}, 1) + + queueMu sync.Mutex + queueLastSubmittedHash common.Hash + queueBestEntry blockQueueEntry + ) + + log.Debug("runBuildingJob", "slot", attrs.Slot, "parent", attrs.HeadHash, "payloadTimestamp", uint64(attrs.Timestamp)) + + submitBestBlock := func() { + queueMu.Lock() + if queueBestEntry.block.Hash() != queueLastSubmittedHash { + err := cb.onSealedBlock(queueBestEntry.block, queueBestEntry.blockValue, queueBestEntry.ordersCloseTime, queueBestEntry.sealedAt, + queueBestEntry.commitedBundles, queueBestEntry.allBundles, queueBestEntry.usedSbundles, attrs) + + if err != nil { + log.Error("could not run sealed block hook", "err", err) + } else { + queueLastSubmittedHash = queueBestEntry.block.Hash() + } + } + queueMu.Unlock() + } + + // Avoid submitting early into a given slot. For example if slots have 12 second interval, submissions should + // not begin until 8 seconds into the slot. + slotTime := time.Unix(int64(attrs.Timestamp), 0).UTC() + slotSubmitStartTime := slotTime.Add(-cb.submissionOffsetFromEndOfSlot) + + // Empties queue, submits the best block for current job with rate limit (global for all jobs) + go runResubmitLoop(ctx, cb.limiter, queueSignal, submitBestBlock, slotSubmitStartTime) + + // Populates queue with submissions that increase block profit + blockHook := func(block *types.Block, blockValue *big.Int, ordersCloseTime time.Time, + committedBundles, allBundles []types.SimulatedBundle, usedSbundles []types.UsedSBundle, + ) { + if ctx.Err() != nil { + return + } + + sealedAt := time.Now() + + queueMu.Lock() + defer queueMu.Unlock() + if block.Hash() != queueLastSubmittedHash { + queueBestEntry = blockQueueEntry{ + block: block, + blockValue: new(big.Int).Set(blockValue), + ordersCloseTime: ordersCloseTime, + sealedAt: sealedAt, + commitedBundles: committedBundles, + allBundles: allBundles, + usedSbundles: usedSbundles, + } + + select { + case queueSignal <- struct{}{}: + default: + } + } + } + + // resubmits block builder requests every builderBlockResubmitInterval + runRetryLoop(ctx, cb.builderResubmitInterval, func() { + log.Debug("retrying BuildBlock", + "slot", attrs.Slot, + "parent", attrs.HeadHash, + "resubmit-interval", cb.builderResubmitInterval.String()) + err := cb.eth.BuildBlock(attrs, blockHook) + if err != nil { + log.Warn("Failed to build block", "err", err) + } + }) +} + +func (cb *CliqueBuilder) onSealedBlock(block *types.Block, blockValue *big.Int, ordersClosedAt, sealedAt time.Time, + commitedBundles, allBundles []types.SimulatedBundle, usedSbundles []types.UsedSBundle, attrs *types.BuilderPayloadAttributes) error { + log.Info("submitted block", "slot", attrs.Slot, "value", blockValue.String(), "parent", block.ParentHash, + "hash", block.Hash(), "#commitedBundles", len(commitedBundles)) + + if cb.eth.Config().IsShanghai(block.Time()) { + if err := cb.submitCapellaBlock(block, blockValue, ordersClosedAt, sealedAt, commitedBundles, allBundles, usedSbundles, attrs); err != nil { + return err + } + } else { + if err := cb.submitBellatrixBlock(block, blockValue, ordersClosedAt, sealedAt, commitedBundles, allBundles, usedSbundles, attrs); err != nil { + return err + } + } + + log.Info("submitted block", "slot", attrs.Slot, "value", blockValue.String(), "parent", block.ParentHash, + "hash", block.Hash(), "#commitedBundles", len(commitedBundles)) + + return nil +} + +func (cb *CliqueBuilder) Stop() error { + close(cb.stop) + return nil +} + +func (cb *CliqueBuilder) OnPayloadAttribute(attrs *types.BuilderPayloadAttributes) error { + // Not implemented for clique. + return nil +} + +func (cb *CliqueBuilder) submitBellatrixBlock(block *types.Block, blockValue *big.Int, ordersClosedAt, sealedAt time.Time, + commitedBundles, allBundles []types.SimulatedBundle, usedSbundles []types.UsedSBundle, + attrs *types.BuilderPayloadAttributes) error { + executableData := engine.BlockToExecutableData(block, blockValue) + payload, err := executableDataToExecutionPayload(executableData.ExecutionPayload) + if err != nil { + log.Error("could not format execution payload", "err", err) + return err + } + + value, overflow := uint256.FromBig(blockValue) + if overflow { + log.Error("could not set block value due to value overflow") + return err + } + + blockBidMsg := apiv1.BidTrace{ + Slot: attrs.Slot, + ParentHash: payload.ParentHash, + BlockHash: payload.BlockHash, + BuilderPubkey: cb.builderPublicKey, + ProposerPubkey: cb.proposerPubkey, + ProposerFeeRecipient: cb.proposerFeeRecipient, + GasLimit: executableData.ExecutionPayload.GasLimit, + GasUsed: executableData.ExecutionPayload.GasUsed, + Value: value, + } + + signature, err := ssz.SignMessage(&blockBidMsg, cb.builderSigningDomain, cb.builderSecretKey) + if err != nil { + log.Error("could not sign builder bid", "err", err) + return err + } + + blockSubmitReq := bellatrixapi.SubmitBlockRequest{ + Signature: signature, + Message: &blockBidMsg, + ExecutionPayload: payload, + } + + go cb.ds.ConsumeBuiltBlock(block, blockValue, ordersClosedAt, sealedAt, commitedBundles, allBundles, usedSbundles, &blockBidMsg) + err = cb.relay.SubmitBlock(&blockSubmitReq, cb.getValidatorData()) + if err != nil { + log.Error("could not submit bellatrix block", "err", err, "#commitedBundles", len(commitedBundles)) + return err + } + + log.Info("submitted bellatrix block", "slot", blockBidMsg.Slot, "value", blockBidMsg.Value.String(), "parent", blockBidMsg.ParentHash, "hash", block.Hash(), "#commitedBundles", len(commitedBundles)) + + return nil +} + +func (cb *CliqueBuilder) submitCapellaBlock(block *types.Block, blockValue *big.Int, ordersClosedAt, sealedAt time.Time, + commitedBundles, allBundles []types.SimulatedBundle, usedSbundles []types.UsedSBundle, + attrs *types.BuilderPayloadAttributes) error { + executableData := engine.BlockToExecutableData(block, blockValue) + payload, err := executableDataToCapellaExecutionPayload(executableData.ExecutionPayload) + if err != nil { + log.Error("could not format execution payload", "err", err) + return err + } + + value, overflow := uint256.FromBig(blockValue) + if overflow { + log.Error("could not set block value due to value overflow") + return err + } + + blockBidMsg := apiv1.BidTrace{ + Slot: attrs.Slot, + ParentHash: payload.ParentHash, + BlockHash: payload.BlockHash, + BuilderPubkey: cb.builderPublicKey, + ProposerPubkey: cb.proposerPubkey, + ProposerFeeRecipient: cb.proposerFeeRecipient, + GasLimit: executableData.ExecutionPayload.GasLimit, + GasUsed: executableData.ExecutionPayload.GasUsed, + Value: value, + } + + signature, err := ssz.SignMessage(&blockBidMsg, cb.builderSigningDomain, cb.builderSecretKey) + if err != nil { + log.Error("could not sign builder bid", "err", err) + return err + } + + blockSubmitReq := capellaapi.SubmitBlockRequest{ + Signature: signature, + Message: &blockBidMsg, + ExecutionPayload: payload, + } + + go cb.ds.ConsumeBuiltBlock(block, blockValue, ordersClosedAt, sealedAt, commitedBundles, allBundles, usedSbundles, &blockBidMsg) + err = cb.relay.SubmitBlockCapella(&blockSubmitReq, cb.getValidatorData()) + if err != nil { + log.Error("could not submit capella block", "err", err, "#commitedBundles", len(commitedBundles)) + return err + } + + log.Info("submitted capella block", "slot", blockBidMsg.Slot, "value", blockBidMsg.Value.String(), "parent", blockBidMsg.ParentHash, "hash", block.Hash(), "#commitedBundles", len(commitedBundles)) + return nil +} diff --git a/builder/config.go b/builder/config.go new file mode 100644 index 0000000000..2bef45e542 --- /dev/null +++ b/builder/config.go @@ -0,0 +1,66 @@ +package builder + +import "time" + +type Config struct { + Enabled bool `toml:",omitempty"` + EnableValidatorChecks bool `toml:",omitempty"` + EnableLocalRelay bool `toml:",omitempty"` + SlotsInEpoch uint64 `toml:",omitempty"` + SecondsInSlot uint64 `toml:",omitempty"` + DisableBundleFetcher bool `toml:",omitempty"` + DryRun bool `toml:",omitempty"` + IgnoreLatePayloadAttributes bool `toml:",omitempty"` + BuilderSecretKey string `toml:",omitempty"` + RelaySecretKey string `toml:",omitempty"` + ListenAddr string `toml:",omitempty"` + GenesisForkVersion string `toml:",omitempty"` + BellatrixForkVersion string `toml:",omitempty"` + GenesisValidatorsRoot string `toml:",omitempty"` + BeaconEndpoints []string `toml:",omitempty"` + RemoteRelayEndpoint string `toml:",omitempty"` + SecondaryRemoteRelayEndpoints []string `toml:",omitempty"` + ValidationBlocklist string `toml:",omitempty"` + ValidationUseCoinbaseDiff bool `toml:",omitempty"` + BuilderRateLimitDuration string `toml:",omitempty"` + BuilderRateLimitMaxBurst int `toml:",omitempty"` + BuilderRateLimitResubmitInterval string `toml:",omitempty"` + BuilderSubmissionOffset time.Duration `toml:",omitempty"` + DiscardRevertibleTxOnErr bool `toml:",omitempty"` + EnableCancellations bool `toml:",omitempty"` + ProposerPubkey string `toml:",omitempty"` +} + +// DefaultConfig is the default config for the builder. +var DefaultConfig = Config{ + Enabled: false, + EnableValidatorChecks: false, + EnableLocalRelay: false, + SlotsInEpoch: 32, + SecondsInSlot: 12, + DisableBundleFetcher: false, + DryRun: false, + IgnoreLatePayloadAttributes: false, + BuilderSecretKey: "0x2fc12ae741f29701f8e30f5de6350766c020cb80768a0ff01e6838ffd2431e11", + RelaySecretKey: "0x2fc12ae741f29701f8e30f5de6350766c020cb80768a0ff01e6838ffd2431e11", + ListenAddr: ":28545", + GenesisForkVersion: "0x00000000", + BellatrixForkVersion: "0x02000000", + GenesisValidatorsRoot: "0x0000000000000000000000000000000000000000000000000000000000000000", + BeaconEndpoints: []string{"http://127.0.0.1:5052"}, + RemoteRelayEndpoint: "", + SecondaryRemoteRelayEndpoints: nil, + ValidationBlocklist: "", + ValidationUseCoinbaseDiff: false, + BuilderRateLimitDuration: RateLimitIntervalDefault.String(), + BuilderRateLimitMaxBurst: RateLimitBurstDefault, + DiscardRevertibleTxOnErr: false, + EnableCancellations: false, +} + +// RelayConfig is the config for a single remote relay. +type RelayConfig struct { + Endpoint string + SszEnabled bool + GzipEnabled bool +} diff --git a/builder/eth_service.go b/builder/eth_service.go new file mode 100644 index 0000000000..26e3cf9379 --- /dev/null +++ b/builder/eth_service.go @@ -0,0 +1,104 @@ +package builder + +import ( + "errors" + "math/big" + "time" + + "github.com/ethereum/go-ethereum/beacon/engine" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/eth" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/miner" + "github.com/ethereum/go-ethereum/params" +) + +type IEthereumService interface { + BuildBlock(attrs *types.BuilderPayloadAttributes, sealedBlockCallback miner.BlockHookFn) error + GetBlockByHash(hash common.Hash) *types.Block + Config() *params.ChainConfig + Synced() bool +} + +type testEthereumService struct { + synced bool + testExecutableData *engine.ExecutableData + testBlock *types.Block + testBlockValue *big.Int + testBundlesMerged []types.SimulatedBundle + testAllBundles []types.SimulatedBundle + testUsedSbundles []types.UsedSBundle +} + +func (t *testEthereumService) BuildBlock(attrs *types.BuilderPayloadAttributes, sealedBlockCallback miner.BlockHookFn) error { + sealedBlockCallback(t.testBlock, t.testBlockValue, time.Now(), t.testBundlesMerged, t.testAllBundles, t.testUsedSbundles) + return nil +} + +func (t *testEthereumService) GetBlockByHash(hash common.Hash) *types.Block { return t.testBlock } + +func (t *testEthereumService) Config() *params.ChainConfig { return params.TestChainConfig } + +func (t *testEthereumService) Synced() bool { return t.synced } + +type EthereumService struct { + eth *eth.Ethereum +} + +func NewEthereumService(eth *eth.Ethereum) *EthereumService { + return &EthereumService{eth: eth} +} + +// TODO: we should move to a setup similar to catalyst local blocks & payload ids +func (s *EthereumService) BuildBlock(attrs *types.BuilderPayloadAttributes, sealedBlockCallback miner.BlockHookFn) error { + // Send a request to generate a full block in the background. + // The result can be obtained via the returned channel. + args := &miner.BuildPayloadArgs{ + Parent: attrs.HeadHash, + Timestamp: uint64(attrs.Timestamp), + FeeRecipient: attrs.SuggestedFeeRecipient, + GasLimit: attrs.GasLimit, + Random: attrs.Random, + Withdrawals: attrs.Withdrawals, + BlockHook: sealedBlockCallback, + } + + payload, err := s.eth.Miner().BuildPayload(args) + if err != nil { + log.Error("Failed to build payload", "err", err) + return err + } + + resCh := make(chan *engine.ExecutionPayloadEnvelope, 1) + go func() { + resCh <- payload.ResolveFull() + }() + + timer := time.NewTimer(4 * time.Second) + defer timer.Stop() + + select { + case payload := <-resCh: + if payload == nil { + return errors.New("received nil payload from sealing work") + } + return nil + case <-timer.C: + payload.Cancel() + log.Error("timeout waiting for block", "parent hash", attrs.HeadHash, "slot", attrs.Slot) + return errors.New("timeout waiting for block result") + } +} + +func (s *EthereumService) GetBlockByHash(hash common.Hash) *types.Block { + return s.eth.BlockChain().GetBlockByHash(hash) +} + +func (s *EthereumService) Config() *params.ChainConfig { + return s.eth.BlockChain().Config() +} + +func (s *EthereumService) Synced() bool { + return s.eth.Synced() +} diff --git a/builder/eth_service_test.go b/builder/eth_service_test.go new file mode 100644 index 0000000000..b1f3dff65c --- /dev/null +++ b/builder/eth_service_test.go @@ -0,0 +1,109 @@ +package builder + +import ( + "math/big" + "testing" + "time" + + "github.com/ethereum/go-ethereum/beacon/engine" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/consensus/ethash" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/eth" + "github.com/ethereum/go-ethereum/eth/downloader" + "github.com/ethereum/go-ethereum/eth/ethconfig" + "github.com/ethereum/go-ethereum/node" + "github.com/ethereum/go-ethereum/p2p" + "github.com/ethereum/go-ethereum/params" + "github.com/stretchr/testify/require" +) + +func generatePreMergeChain(n int) (*core.Genesis, []*types.Block) { + db := rawdb.NewMemoryDatabase() + config := params.AllEthashProtocolChanges + genesis := &core.Genesis{ + Config: config, + Alloc: core.GenesisAlloc{}, + ExtraData: []byte("test genesis"), + Timestamp: 9000, + BaseFee: big.NewInt(params.InitialBaseFee), + Difficulty: big.NewInt(0), + } + gblock := genesis.ToBlock() + engine := ethash.NewFaker() + blocks, _ := core.GenerateChain(config, gblock, engine, db, n, nil) + totalDifficulty := big.NewInt(0) + for _, b := range blocks { + totalDifficulty.Add(totalDifficulty, b.Difficulty()) + } + config.TerminalTotalDifficulty = totalDifficulty + return genesis, blocks +} + +// startEthService creates a full node instance for testing. +func startEthService(t *testing.T, genesis *core.Genesis, blocks []*types.Block) (*node.Node, *eth.Ethereum) { + t.Helper() + + n, err := node.New(&node.Config{ + P2P: p2p.Config{ + ListenAddr: "0.0.0.0:0", + NoDiscovery: true, + MaxPeers: 25, + }, + }) + if err != nil { + t.Fatal("can't create node:", err) + } + + ethcfg := ðconfig.Config{Genesis: genesis, Ethash: ethash.Config{PowMode: ethash.ModeFake}, SyncMode: downloader.SnapSync, TrieTimeout: time.Minute, TrieDirtyCache: 256, TrieCleanCache: 256} + ethservice, err := eth.New(n, ethcfg) + if err != nil { + t.Fatal("can't create eth service:", err) + } + if err := n.Start(); err != nil { + t.Fatal("can't start node:", err) + } + if _, err := ethservice.BlockChain().InsertChain(blocks); err != nil { + n.Close() + t.Fatal("can't import test blocks:", err) + } + time.Sleep(500 * time.Millisecond) // give txpool enough time to consume head event + + ethservice.SetSynced() + return n, ethservice +} + +func TestBuildBlock(t *testing.T) { + genesis, blocks := generatePreMergeChain(10) + n, ethservice := startEthService(t, genesis, blocks) + defer n.Close() + + parent := ethservice.BlockChain().CurrentBlock() + + testPayloadAttributes := &types.BuilderPayloadAttributes{ + Timestamp: hexutil.Uint64(parent.Time + 1), + Random: common.Hash{0x05, 0x10}, + SuggestedFeeRecipient: common.Address{0x04, 0x10}, + GasLimit: uint64(4800000), + Slot: uint64(25), + } + + service := NewEthereumService(ethservice) + service.eth.APIBackend.Miner().SetEtherbase(common.Address{0x05, 0x11}) + + err := service.BuildBlock(testPayloadAttributes, func(block *types.Block, blockValue *big.Int, _ time.Time, _, _ []types.SimulatedBundle, _ []types.UsedSBundle) { + executableData := engine.BlockToExecutableData(block, blockValue) + require.Equal(t, common.Address{0x05, 0x11}, executableData.ExecutionPayload.FeeRecipient) + require.Equal(t, common.Hash{0x05, 0x10}, executableData.ExecutionPayload.Random) + require.Equal(t, parent.Hash(), executableData.ExecutionPayload.ParentHash) + require.Equal(t, parent.Time+1, executableData.ExecutionPayload.Timestamp) + require.Equal(t, block.ParentHash(), parent.Hash()) + require.Equal(t, block.Hash(), executableData.ExecutionPayload.BlockHash) + require.Equal(t, blockValue.Uint64(), uint64(0)) + }) + + require.NoError(t, err) +} diff --git a/builder/files/bor-post-install.sh b/builder/files/bor-post-install.sh deleted file mode 100644 index 1419479983..0000000000 --- a/builder/files/bor-post-install.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/sh -set -e - -PKG="bor" - -if ! getent passwd $PKG >/dev/null ; then - adduser --disabled-password --disabled-login --shell /usr/sbin/nologin --quiet --system --no-create-home --home /nonexistent $PKG - echo "Created system user $PKG" -fi diff --git a/builder/files/bor.service b/builder/files/bor.service deleted file mode 100644 index 758553299e..0000000000 --- a/builder/files/bor.service +++ /dev/null @@ -1,16 +0,0 @@ -[Unit] - Description=bor - StartLimitIntervalSec=500 - StartLimitBurst=5 - -[Service] - Restart=on-failure - RestartSec=5s - ExecStart=/usr/local/bin/bor server -config "/var/lib/bor/config.toml" - Type=simple - User=bor - KillSignal=SIGINT - TimeoutStopSec=120 - -[Install] - WantedBy=multi-user.target diff --git a/builder/files/config.toml b/builder/files/config.toml deleted file mode 100644 index 61b3796984..0000000000 --- a/builder/files/config.toml +++ /dev/null @@ -1,186 +0,0 @@ -# NOTE: Uncomment and configure the following 8 fields in case you run a validator: -# `mine`, `etherbase`, `nodiscover`, `maxpeers`, `keystore`, `allow-insecure-unlock`, `password`, `unlock` - -chain = "mainnet" -# chain = "mumbai" -# identity = "Annon-Identity" -# verbosity = 3 -# vmdebug = false -datadir = "/var/lib/bor/data" -# ancient = "" -# db.engine = "leveldb" -# keystore = "/var/lib/bor/keystore" -# "rpc.batchlimit" = 100 -# "rpc.returndatalimit" = 100000 -syncmode = "full" -# gcmode = "full" -# snapshot = true -# "bor.logs" = false -# ethstats = "" -# devfakeauthor = false -# ["eth.requiredblocks"] - -# [log] - # vmodule = "" - # json = false - # backtrace = "" - # debug = true - -[p2p] - # maxpeers = 1 - # nodiscover = true - # maxpendpeers = 50 - # bind = "0.0.0.0" - # port = 30303 - # nat = "any" - # netrestrict = "" - # nodekey = "" - # nodekeyhex = "" - [p2p.discovery] - # v5disc = false - bootnodes = ["enode://b8f1cc9c5d4403703fbf377116469667d2b1823c0daf16b7250aa576bacf399e42c3930ccfcb02c5df6879565a2b8931335565f0e8d3f8e72385ecf4a4bf160a@3.36.224.80:30303", "enode://8729e0c825f3d9cad382555f3e46dcff21af323e89025a0e6312df541f4a9e73abfa562d64906f5e59c51fe6f0501b3e61b07979606c56329c020ed739910759@54.194.245.5:30303"] - # Uncomment below `bootnodes` field for Mumbai bootnode - # bootnodes = ["enode://bdcd4786a616a853b8a041f53496d853c68d99d54ff305615cd91c03cd56895e0a7f6e9f35dbf89131044e2114a9a782b792b5661e3aff07faf125a98606a071@43.200.206.40:30303", "enode://209aaf7ed549cf4a5700fd833da25413f80a1248bd3aa7fe2a87203e3f7b236dd729579e5c8df61c97bf508281bae4969d6de76a7393bcbd04a0af70270333b3@54.216.248.9:30303"] - # bootnodesv4 = [] - # bootnodesv5 = [] - # static-nodes = [] - # trusted-nodes = [] - # dns = [] - -# [heimdall] - # url = "http://localhost:1317" - # "bor.without" = false - # grpc-address = "" - -[txpool] - nolocals = true - pricelimit = 30000000000 - accountslots = 16 - globalslots = 32768 - accountqueue = 16 - globalqueue = 32768 - lifetime = "1h30m0s" - # locals = [] - # journal = "" - # rejournal = "1h0m0s" - # pricebump = 10 - -[miner] - gaslimit = 30000000 - gasprice = "30000000000" - # mine = true - # etherbase = "VALIDATOR ADDRESS" - # extradata = "" - # recommit = "2m5s" - # commitinterrupt = true - - -# [jsonrpc] -# ipcdisable = false -# ipcpath = "" -# gascap = 50000000 -# evmtimeout = "5s" -# txfeecap = 5.0 -# allow-unprotected-txs = false -# enabledeprecatedpersonal = false -# [jsonrpc.http] -# enabled = false -# port = 8545 -# prefix = "" -# host = "localhost" -# api = ["eth", "net", "web3", "txpool", "bor"] -# vhosts = ["*"] -# corsdomain = ["*"] -# ep-size = 40 -# ep-requesttimeout = "0s" -# [jsonrpc.ws] -# enabled = false -# port = 8546 -# prefix = "" -# host = "localhost" -# api = ["web3", "net"] -# origins = ["*"] -# ep-size = 40 -# ep-requesttimeout = "0s" -# [jsonrpc.graphql] -# enabled = false -# port = 0 -# prefix = "" -# host = "" -# vhosts = ["*"] -# corsdomain = ["*"] -# [jsonrpc.auth] -# jwtsecret = "" -# addr = "localhost" -# port = 8551 -# vhosts = ["localhost"] -# [jsonrpc.timeouts] -# read = "10s" -# write = "30s" -# idle = "2m0s" - -[gpo] - # blocks = 20 - # percentile = 60 - # maxheaderhistory = 1024 - # maxblockhistory = 1024 - # maxprice = "5000000000000" - ignoreprice = "30000000000" - -[telemetry] - metrics = true - # expensive = false - # prometheus-addr = "127.0.0.1:7071" - # opencollector-endpoint = "" - # [telemetry.influx] - # influxdb = false - # endpoint = "" - # database = "" - # username = "" - # password = "" - # influxdbv2 = false - # token = "" - # bucket = "" - # organization = "" - # [telemetry.influx.tags] - -# [cache] - # cache = 1024 - # gc = 25 - # snapshot = 10 - # database = 50 - # trie = 15 - # journal = "triecache" - # rejournal = "1h0m0s" - # noprefetch = false - # preimages = false - # txlookuplimit = 2350000 - # triesinmemory = 128 - # timeout = "1h0m0s" - # fdlimit = 0 - -[accounts] - # allow-insecure-unlock = true - # password = "/var/lib/bor/password.txt" - # unlock = ["VALIDATOR ADDRESS"] - # lightkdf = false - # disable-bor-wallet = false - -# [grpc] - # addr = ":3131" - -# [developer] - # dev = false - # period = 0 - # gaslimit = 11500000 - -# [parallelevm] - # enable = true - # procs = 8 - -# [pprof] -# pprof = false -# port = 6060 -# addr = "127.0.0.1" -# memprofilerate = 524288 -# blockprofilerate = 0 diff --git a/builder/files/genesis-mainnet-v1.json b/builder/files/genesis-mainnet-v1.json deleted file mode 100644 index d4c89c67f0..0000000000 --- a/builder/files/genesis-mainnet-v1.json +++ /dev/null @@ -1,109 +0,0 @@ -{ - "config": { - "chainId": 137, - "homesteadBlock": 0, - "eip150Block": 0, - "eip150Hash": "0x0000000000000000000000000000000000000000000000000000000000000000", - "eip155Block": 0, - "eip158Block": 0, - "byzantiumBlock": 0, - "constantinopleBlock": 0, - "petersburgBlock": 0, - "istanbulBlock": 3395000, - "muirGlacierBlock": 3395000, - "berlinBlock": 14750000, - "londonBlock": 23850000, - "bor": { - "jaipurBlock": 23850000, - "delhiBlock": 38189056, - "parallelUniverseBlock": 0, - "indoreBlock": 44934656, - "stateSyncConfirmationDelay": { - "44934656": 128 - }, - "period": { - "0": 2 - }, - "producerDelay": { - "0": 6, - "38189056": 4 - }, - "sprint": { - "0": 64, - "38189056": 16 - }, - "backupMultiplier": { - "0": 2 - }, - "validatorContract": "0x0000000000000000000000000000000000001000", - "stateReceiverContract": "0x0000000000000000000000000000000000001001", - "overrideStateSyncRecords": { - "14949120": 8, - "14949184": 0, - "14953472": 0, - "14953536": 5, - "14953600": 0, - "14953664": 0, - "14953728": 0, - "14953792": 0, - "14953856": 0 - }, - "blockAlloc": { - "22156660": { - "0000000000000000000000000000000000001010": { - "balance": "0x0", - "code": "0x60806040526004361061019c5760003560e01c806377d32e94116100ec578063acd06cb31161008a578063e306f77911610064578063e306f77914610a7b578063e614d0d614610aa6578063f2fde38b14610ad1578063fc0c546a14610b225761019c565b8063acd06cb31461097a578063b789543c146109cd578063cc79f97b14610a505761019c565b80639025e64c116100c65780639025e64c146107c957806395d89b4114610859578063a9059cbb146108e9578063abceeba21461094f5761019c565b806377d32e94146106315780638da5cb5b146107435780638f32d59b1461079a5761019c565b806347e7ef24116101595780637019d41a116101335780637019d41a1461053357806370a082311461058a578063715018a6146105ef578063771282f6146106065761019c565b806347e7ef2414610410578063485cc9551461046b57806360f96a8f146104dc5761019c565b806306fdde03146101a15780631499c5921461023157806318160ddd1461028257806319d27d9c146102ad5780632e1a7d4d146103b1578063313ce567146103df575b600080fd5b3480156101ad57600080fd5b506101b6610b79565b6040518080602001828103825283818151815260200191508051906020019080838360005b838110156101f65780820151818401526020810190506101db565b50505050905090810190601f1680156102235780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b34801561023d57600080fd5b506102806004803603602081101561025457600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff169060200190929190505050610bb6565b005b34801561028e57600080fd5b50610297610c24565b6040518082815260200191505060405180910390f35b3480156102b957600080fd5b5061036f600480360360a08110156102d057600080fd5b81019080803590602001906401000000008111156102ed57600080fd5b8201836020820111156102ff57600080fd5b8035906020019184600183028401116401000000008311171561032157600080fd5b9091929391929390803590602001909291908035906020019092919080359060200190929190803573ffffffffffffffffffffffffffffffffffffffff169060200190929190505050610c3a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b6103dd600480360360208110156103c757600080fd5b8101908080359060200190929190505050610caa565b005b3480156103eb57600080fd5b506103f4610dfc565b604051808260ff1660ff16815260200191505060405180910390f35b34801561041c57600080fd5b506104696004803603604081101561043357600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff16906020019092919080359060200190929190505050610e05565b005b34801561047757600080fd5b506104da6004803603604081101561048e57600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff169060200190929190803573ffffffffffffffffffffffffffffffffffffffff169060200190929190505050610fc1565b005b3480156104e857600080fd5b506104f1611090565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b34801561053f57600080fd5b506105486110b6565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b34801561059657600080fd5b506105d9600480360360208110156105ad57600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff1690602001909291905050506110dc565b6040518082815260200191505060405180910390f35b3480156105fb57600080fd5b506106046110fd565b005b34801561061257600080fd5b5061061b6111cd565b6040518082815260200191505060405180910390f35b34801561063d57600080fd5b506107016004803603604081101561065457600080fd5b81019080803590602001909291908035906020019064010000000081111561067b57600080fd5b82018360208201111561068d57600080fd5b803590602001918460018302840111640100000000831117156106af57600080fd5b91908080601f016020809104026020016040519081016040528093929190818152602001838380828437600081840152601f19601f8201169050808301925050505050505091929192905050506111d3565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b34801561074f57600080fd5b50610758611358565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b3480156107a657600080fd5b506107af611381565b604051808215151515815260200191505060405180910390f35b3480156107d557600080fd5b506107de6113d8565b6040518080602001828103825283818151815260200191508051906020019080838360005b8381101561081e578082015181840152602081019050610803565b50505050905090810190601f16801561084b5780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b34801561086557600080fd5b5061086e611411565b6040518080602001828103825283818151815260200191508051906020019080838360005b838110156108ae578082015181840152602081019050610893565b50505050905090810190601f1680156108db5780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b610935600480360360408110156108ff57600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff1690602001909291908035906020019092919050505061144e565b604051808215151515815260200191505060405180910390f35b34801561095b57600080fd5b50610964611474565b6040518082815260200191505060405180910390f35b34801561098657600080fd5b506109b36004803603602081101561099d57600080fd5b8101908080359060200190929190505050611501565b604051808215151515815260200191505060405180910390f35b3480156109d957600080fd5b50610a3a600480360360808110156109f057600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff169060200190929190803590602001909291908035906020019092919080359060200190929190505050611521565b6040518082815260200191505060405180910390f35b348015610a5c57600080fd5b50610a65611541565b6040518082815260200191505060405180910390f35b348015610a8757600080fd5b50610a90611546565b6040518082815260200191505060405180910390f35b348015610ab257600080fd5b50610abb61154c565b6040518082815260200191505060405180910390f35b348015610add57600080fd5b50610b2060048036036020811015610af457600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff1690602001909291905050506115d9565b005b348015610b2e57600080fd5b50610b376115f6565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b60606040518060400160405280600b81526020017f4d6174696320546f6b656e000000000000000000000000000000000000000000815250905090565b6040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260108152602001807f44697361626c656420666561747572650000000000000000000000000000000081525060200191505060405180910390fd5b6000601260ff16600a0a6402540be40002905090565b60006040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260108152602001807f44697361626c656420666561747572650000000000000000000000000000000081525060200191505060405180910390fd5b60003390506000610cba826110dc565b9050610cd18360065461161c90919063ffffffff16565b600681905550600083118015610ce657508234145b610d58576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260138152602001807f496e73756666696369656e7420616d6f756e740000000000000000000000000081525060200191505060405180910390fd5b8173ffffffffffffffffffffffffffffffffffffffff16600260009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff167febff2602b3f468259e1e99f613fed6691f3a6526effe6ef3e768ba7ae7a36c4f8584610dd4876110dc565b60405180848152602001838152602001828152602001935050505060405180910390a3505050565b60006012905090565b610e0d611381565b610e1657600080fd5b600081118015610e535750600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff1614155b610ea8576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526023815260200180611da76023913960400191505060405180910390fd5b6000610eb3836110dc565b905060008390508073ffffffffffffffffffffffffffffffffffffffff166108fc849081150290604051600060405180830381858888f19350505050158015610f00573d6000803e3d6000fd5b50610f168360065461163c90919063ffffffff16565b6006819055508373ffffffffffffffffffffffffffffffffffffffff16600260009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff167f4e2ca0515ed1aef1395f66b5303bb5d6f1bf9d61a353fa53f73f8ac9973fa9f68585610f98896110dc565b60405180848152602001838152602001828152602001935050505060405180910390a350505050565b600760009054906101000a900460ff1615611027576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526023815260200180611d846023913960400191505060405180910390fd5b6001600760006101000a81548160ff02191690831515021790555080600260006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555061108c8261165b565b5050565b600360009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b600460009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b60008173ffffffffffffffffffffffffffffffffffffffff16319050919050565b611105611381565b61110e57600080fd5b600073ffffffffffffffffffffffffffffffffffffffff166000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff167f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e060405160405180910390a360008060006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550565b60065481565b60008060008060418551146111ee5760009350505050611352565b602085015192506040850151915060ff6041860151169050601b8160ff16101561121957601b810190505b601b8160ff16141580156112315750601c8160ff1614155b156112425760009350505050611352565b60018682858560405160008152602001604052604051808581526020018460ff1660ff1681526020018381526020018281526020019450505050506020604051602081039080840390855afa15801561129f573d6000803e3d6000fd5b505050602060405103519350600073ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff16141561134e576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260128152602001807f4572726f7220696e2065637265636f766572000000000000000000000000000081525060200191505060405180910390fd5b5050505b92915050565b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff16905090565b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614905090565b6040518060400160405280600181526020017f890000000000000000000000000000000000000000000000000000000000000081525081565b60606040518060400160405280600581526020017f4d41544943000000000000000000000000000000000000000000000000000000815250905090565b6000813414611460576000905061146e565b61146b338484611753565b90505b92915050565b6040518060800160405280605b8152602001611e1c605b91396040516020018082805190602001908083835b602083106114c357805182526020820191506020810190506020830392506114a0565b6001836020036101000a0380198251168184511680821785525050505050509050019150506040516020818303038152906040528051906020012081565b60056020528060005260406000206000915054906101000a900460ff1681565b600061153761153286868686611b10565b611be6565b9050949350505050565b608981565b60015481565b604051806080016040528060528152602001611dca605291396040516020018082805190602001908083835b6020831061159b5780518252602082019150602081019050602083039250611578565b6001836020036101000a0380198251168184511680821785525050505050509050019150506040516020818303038152906040528051906020012081565b6115e1611381565b6115ea57600080fd5b6115f38161165b565b50565b600260009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b60008282111561162b57600080fd5b600082840390508091505092915050565b60008082840190508381101561165157600080fd5b8091505092915050565b600073ffffffffffffffffffffffffffffffffffffffff168173ffffffffffffffffffffffffffffffffffffffff16141561169557600080fd5b8073ffffffffffffffffffffffffffffffffffffffff166000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff167f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e060405160405180910390a3806000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555050565b6000803073ffffffffffffffffffffffffffffffffffffffff166370a08231866040518263ffffffff1660e01b8152600401808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060206040518083038186803b1580156117d357600080fd5b505afa1580156117e7573d6000803e3d6000fd5b505050506040513d60208110156117fd57600080fd5b8101908080519060200190929190505050905060003073ffffffffffffffffffffffffffffffffffffffff166370a08231866040518263ffffffff1660e01b8152600401808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060206040518083038186803b15801561188f57600080fd5b505afa1580156118a3573d6000803e3d6000fd5b505050506040513d60208110156118b957600080fd5b810190808051906020019092919050505090506118d7868686611c30565b8473ffffffffffffffffffffffffffffffffffffffff168673ffffffffffffffffffffffffffffffffffffffff16600260009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff167fe6497e3ee548a3372136af2fcb0696db31fc6cf20260707645068bd3fe97f3c48786863073ffffffffffffffffffffffffffffffffffffffff166370a082318e6040518263ffffffff1660e01b8152600401808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060206040518083038186803b1580156119df57600080fd5b505afa1580156119f3573d6000803e3d6000fd5b505050506040513d6020811015611a0957600080fd5b81019080805190602001909291905050503073ffffffffffffffffffffffffffffffffffffffff166370a082318e6040518263ffffffff1660e01b8152600401808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060206040518083038186803b158015611a9757600080fd5b505afa158015611aab573d6000803e3d6000fd5b505050506040513d6020811015611ac157600080fd5b8101908080519060200190929190505050604051808681526020018581526020018481526020018381526020018281526020019550505050505060405180910390a46001925050509392505050565b6000806040518060800160405280605b8152602001611e1c605b91396040516020018082805190602001908083835b60208310611b625780518252602082019150602081019050602083039250611b3f565b6001836020036101000a03801982511681845116808217855250505050505090500191505060405160208183030381529060405280519060200120905060405181815273ffffffffffffffffffffffffffffffffffffffff8716602082015285604082015284606082015283608082015260a0812092505081915050949350505050565b60008060015490506040517f190100000000000000000000000000000000000000000000000000000000000081528160028201528360228201526042812092505081915050919050565b3073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff161415611cd2576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260138152602001807f63616e27742073656e6420746f204d524332300000000000000000000000000081525060200191505060405180910390fd5b8173ffffffffffffffffffffffffffffffffffffffff166108fc829081150290604051600060405180830381858888f19350505050158015611d18573d6000803e3d6000fd5b508173ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef836040518082815260200191505060405180910390a350505056fe54686520636f6e747261637420697320616c726561647920696e697469616c697a6564496e73756666696369656e7420616d6f756e74206f7220696e76616c69642075736572454950373132446f6d61696e28737472696e67206e616d652c737472696e672076657273696f6e2c75696e7432353620636861696e49642c6164647265737320766572696679696e67436f6e747261637429546f6b656e5472616e736665724f726465722861646472657373207370656e6465722c75696e7432353620746f6b656e49644f72416d6f756e742c6279746573333220646174612c75696e743235362065787069726174696f6e29a265627a7a72315820a4a6f71a98ac3fc613c3a8f1e2e11b9eb9b6b39f125f7d9508916c2b8fb02c7164736f6c63430005100032" - } - } - }, - "burntContract": { - "23850000": "0x70bca57f4579f58670ab2d18ef16e02c17553c38" - } - } - }, - "nonce": "0x0", - "timestamp": "0x5ED20F84", - "extraData": "", - "gasLimit": "0x989680", - "difficulty": "0x1", - "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", - "coinbase": "0x0000000000000000000000000000000000000000", - "alloc": { - "0000000000000000000000000000000000001000": { - "balance": "0x0", - "code": "0x608060405234801561001057600080fd5b50600436106101f05760003560e01c806360c8614d1161010f578063af26aa96116100a2578063d5b844eb11610071578063d5b844eb14610666578063dcf2793a14610684578063e3b7c924146106b6578063f59cf565146106d4576101f0565b8063af26aa96146105c7578063b71d7a69146105e7578063b7ab4db514610617578063c1b3c91914610636576101f0565b806370ba5707116100de57806370ba57071461052b57806398ab2b621461055b5780639d11b80714610579578063ae756451146105a9576101f0565b806360c8614d1461049c57806365b3a1e2146104bc57806366332354146104db578063687a9bd6146104f9576101f0565b80633434735f1161018757806344d6528f1161015657806344d6528f146103ee5780634dbc959f1461041e57806355614fcc1461043c578063582a8d081461046c576101f0565b80633434735f1461035257806335ddfeea1461037057806343ee8213146103a057806344c15cb1146103be576101f0565b806323f2a73f116101c357806323f2a73f146102a45780632bc06564146102d45780632de3a180146102f25780632eddf35214610322576101f0565b8063047a6c5b146101f55780630c35b1cb146102275780631270b5741461025857806323c2a2b414610288575b600080fd5b61020f600480360361020a9190810190612c14565b610706565b60405161021e93929190613553565b60405180910390f35b610241600480360361023c9190810190612c14565b61075d565b60405161024f929190613374565b60405180910390f35b610272600480360361026d9190810190612c3d565b610939565b60405161027f91906133ab565b60405180910390f35b6102a2600480360361029d9190810190612d1c565b610a91565b005b6102be60048036036102b99190810190612c3d565b61112a565b6040516102cb91906133ab565b60405180910390f35b6102dc611281565b6040516102e99190613501565b60405180910390f35b61030c60048036036103079190810190612b71565b611286565b60405161031991906133c6565b60405180910390f35b61033c60048036036103379190810190612c14565b611307565b6040516103499190613501565b60405180910390f35b61035a611437565b6040516103679190613359565b60405180910390f35b61038a60048036036103859190810190612bad565b61144f565b60405161039791906133ab565b60405180910390f35b6103a861151a565b6040516103b591906133c6565b60405180910390f35b6103d860048036036103d39190810190612c79565b611531565b6040516103e59190613501565b60405180910390f35b61040860048036036104039190810190612c3d565b611619565b60405161041591906134e6565b60405180910390f35b610426611781565b6040516104339190613501565b60405180910390f35b61045660048036036104519190810190612af6565b611791565b60405161046391906133ab565b60405180910390f35b61048660048036036104819190810190612b1f565b6117ab565b60405161049391906133c6565b60405180910390f35b6104a4611829565b6040516104b393929190613553565b60405180910390f35b6104c461189d565b6040516104d2929190613374565b60405180910390f35b6104e3611c5e565b6040516104f09190613501565b60405180910390f35b610513600480360361050e9190810190612ce0565b611c63565b6040516105229392919061351c565b60405180910390f35b61054560048036036105409190810190612af6565b611cc7565b60405161055291906133ab565b60405180910390f35b610563611ce1565b60405161057091906133c6565b60405180910390f35b610593600480360361058e9190810190612c14565b611cf8565b6040516105a09190613501565b60405180910390f35b6105b1611e29565b6040516105be91906133c6565b60405180910390f35b6105cf611e40565b6040516105de93929190613553565b60405180910390f35b61060160048036036105fc9190810190612c14565b611ea1565b60405161060e9190613501565b60405180910390f35b61061f611fa1565b60405161062d929190613374565b60405180910390f35b610650600480360361064b9190810190612c14565b611fb5565b60405161065d9190613501565b60405180910390f35b61066e611fd6565b60405161067b919061358a565b60405180910390f35b61069e60048036036106999190810190612ce0565b611fdb565b6040516106ad9392919061351c565b60405180910390f35b6106be61203f565b6040516106cb9190613501565b60405180910390f35b6106ee60048036036106e99190810190612c14565b612051565b6040516106fd93929190613553565b60405180910390f35b60008060006002600085815260200190815260200160002060000154600260008681526020019081526020016000206001015460026000878152602001908152602001600020600201549250925092509193909250565b60608060ff83116107795761077061189d565b91509150610934565b600061078484611ea1565b9050606060016000838152602001908152602001600020805490506040519080825280602002602001820160405280156107cd5781602001602082028038833980820191505090505b509050606060016000848152602001908152602001600020805490506040519080825280602002602001820160405280156108175781602001602082028038833980820191505090505b50905060008090505b60016000858152602001908152602001600020805490508110156109295760016000858152602001908152602001600020818154811061085c57fe5b906000526020600020906003020160020160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1683828151811061089a57fe5b602002602001019073ffffffffffffffffffffffffffffffffffffffff16908173ffffffffffffffffffffffffffffffffffffffff16815250506001600085815260200190815260200160002081815481106108f257fe5b90600052602060002090600302016001015482828151811061091057fe5b6020026020010181815250508080600101915050610820565b508181945094505050505b915091565b6000606060016000858152602001908152602001600020805480602002602001604051908101604052809291908181526020016000905b82821015610a0c578382906000526020600020906003020160405180606001604052908160008201548152602001600182015481526020016002820160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152505081526020019060010190610970565b50505050905060008090505b8151811015610a84578373ffffffffffffffffffffffffffffffffffffffff16828281518110610a4457fe5b60200260200101516040015173ffffffffffffffffffffffffffffffffffffffff161415610a7757600192505050610a8b565b8080600101915050610a18565b5060009150505b92915050565b73fffffffffffffffffffffffffffffffffffffffe73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614610b13576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610b0a906134c6565b60405180910390fd5b6000610b1d611781565b90506000811415610b3157610b3061207b565b5b610b4560018261239c90919063ffffffff16565b8814610b86576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610b7d90613446565b60405180910390fd5b868611610bc8576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610bbf906134a6565b60405180910390fd5b6000604060018989030181610bd957fe5b0614610c1a576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610c1190613486565b60405180910390fd5b8660026000838152602001908152602001600020600101541115610c73576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610c6a90613426565b60405180910390fd5b6000600260008a81526020019081526020016000206000015414610ccc576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610cc390613466565b60405180910390fd5b604051806060016040528089815260200188815260200187815250600260008a8152602001908152602001600020600082015181600001556020820151816001015560408201518160020155905050600388908060018154018082558091505090600182039060005260206000200160009091929091909150555060008060008a815260200190815260200160002081610d6691906128f0565b506000600160008a815260200190815260200160002081610d8791906128f0565b506060610ddf610dda87878080601f016020809104026020016040519081016040528093929190818152602001838380828437600081840152601f19601f820116905080830192505050505050506123bb565b6123e9565b905060008090505b8151811015610f51576060610e0e838381518110610e0157fe5b60200260200101516123e9565b90506000808c81526020019081526020016000208054809190600101610e3491906128f0565b506040518060600160405280610e5d83600081518110610e5057fe5b60200260200101516124c6565b8152602001610e7f83600181518110610e7257fe5b60200260200101516124c6565b8152602001610ea183600281518110610e9457fe5b6020026020010151612537565b73ffffffffffffffffffffffffffffffffffffffff168152506000808d81526020019081526020016000208381548110610ed757fe5b9060005260206000209060030201600082015181600001556020820151816001015560408201518160020160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550905050508080600101915050610de7565b506060610fa9610fa486868080601f016020809104026020016040519081016040528093929190818152602001838380828437600081840152601f19601f820116905080830192505050505050506123bb565b6123e9565b905060008090505b815181101561111d576060610fd8838381518110610fcb57fe5b60200260200101516123e9565b9050600160008d81526020019081526020016000208054809190600101610fff91906128f0565b5060405180606001604052806110288360008151811061101b57fe5b60200260200101516124c6565b815260200161104a8360018151811061103d57fe5b60200260200101516124c6565b815260200161106c8360028151811061105f57fe5b6020026020010151612537565b73ffffffffffffffffffffffffffffffffffffffff16815250600160008e815260200190815260200160002083815481106110a357fe5b9060005260206000209060030201600082015181600001556020820151816001015560408201518160020160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550905050508080600101915050610fb1565b5050505050505050505050565b60006060600080858152602001908152602001600020805480602002602001604051908101604052809291908181526020016000905b828210156111fc578382906000526020600020906003020160405180606001604052908160008201548152602001600182015481526020016002820160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152505081526020019060010190611160565b50505050905060008090505b8151811015611274578373ffffffffffffffffffffffffffffffffffffffff1682828151811061123457fe5b60200260200101516040015173ffffffffffffffffffffffffffffffffffffffff1614156112675760019250505061127b565b8080600101915050611208565b5060009150505b92915050565b604081565b60006002600160f81b84846040516020016112a3939291906132c6565b6040516020818303038152906040526040516112bf9190613303565b602060405180830381855afa1580156112dc573d6000803e3d6000fd5b5050506040513d601f19601f820116820180604052506112ff9190810190612b48565b905092915050565b60006060600080848152602001908152602001600020805480602002602001604051908101604052809291908181526020016000905b828210156113d9578382906000526020600020906003020160405180606001604052908160008201548152602001600182015481526020016002820160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815250508152602001906001019061133d565b505050509050600080905060008090505b825181101561142c5761141d83828151811061140257fe5b6020026020010151602001518361239c90919063ffffffff16565b915080806001019150506113ea565b508092505050919050565b73fffffffffffffffffffffffffffffffffffffffe81565b600080600080859050600060218087518161146657fe5b04029050600081111561147f5761147c876117ab565b91505b6000602190505b818111611509576000600182038801519050818801519550806000602081106114ab57fe5b1a60f81b9450600060f81b857effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff191614156114f0576114e98685611286565b93506114fd565b6114fa8487611286565b93505b50602181019050611486565b508782149450505050509392505050565b60405161152690613344565b604051809103902081565b60008060009050600080905060008090505b84518167ffffffffffffffff16101561160c57606061156e868367ffffffffffffffff16604161255a565b9050600061158582896125e690919063ffffffff16565b905061158f612922565b6115998a83611619565b90506115a58a8361112a565b80156115dc57508473ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff16115b156115fe578194506115fb81602001518761239c90919063ffffffff16565b95505b505050604181019050611543565b5081925050509392505050565b611621612922565b6060600080858152602001908152602001600020805480602002602001604051908101604052809291908181526020016000905b828210156116f1578382906000526020600020906003020160405180606001604052908160008201548152602001600182015481526020016002820160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152505081526020019060010190611655565b50505050905060008090505b8151811015611779578373ffffffffffffffffffffffffffffffffffffffff1682828151811061172957fe5b60200260200101516040015173ffffffffffffffffffffffffffffffffffffffff16141561176c5781818151811061175d57fe5b60200260200101519250611779565b80806001019150506116fd565b505092915050565b600061178c43611ea1565b905090565b60006117a461179e611781565b8361112a565b9050919050565b60006002600060f81b836040516020016117c692919061329a565b6040516020818303038152906040526040516117e29190613303565b602060405180830381855afa1580156117ff573d6000803e3d6000fd5b5050506040513d601f19601f820116820180604052506118229190810190612b48565b9050919050565b60008060008061184a600161183c611781565b61239c90919063ffffffff16565b905060026000828152602001908152602001600020600001546002600083815260200190815260200160002060010154600260008481526020019081526020016000206002015493509350935050909192565b606080606060076040519080825280602002602001820160405280156118d25781602001602082028038833980820191505090505b509050735973918275c01f50555d44e92c9d9b353cadad54816000815181106118f757fe5b602002602001019073ffffffffffffffffffffffffffffffffffffffff16908173ffffffffffffffffffffffffffffffffffffffff168152505073b8bb158b93c94ed35c1970d610d1e2b34e26652c8160018151811061195357fe5b602002602001019073ffffffffffffffffffffffffffffffffffffffff16908173ffffffffffffffffffffffffffffffffffffffff168152505073f84c74dea96df0ec22e11e7c33996c73fcc2d822816002815181106119af57fe5b602002602001019073ffffffffffffffffffffffffffffffffffffffff16908173ffffffffffffffffffffffffffffffffffffffff168152505073b702f1c9154ac9c08da247a8e30ee6f2f3373f4181600381518110611a0b57fe5b602002602001019073ffffffffffffffffffffffffffffffffffffffff16908173ffffffffffffffffffffffffffffffffffffffff1681525050737fcd58c2d53d980b247f1612fdba93e9a76193e681600481518110611a6757fe5b602002602001019073ffffffffffffffffffffffffffffffffffffffff16908173ffffffffffffffffffffffffffffffffffffffff1681525050730375b2fc7140977c9c76d45421564e354ed4227781600581518110611ac357fe5b602002602001019073ffffffffffffffffffffffffffffffffffffffff16908173ffffffffffffffffffffffffffffffffffffffff16815250507342eefcda06ead475cde3731b8eb138e88cd0bac381600681518110611b1f57fe5b602002602001019073ffffffffffffffffffffffffffffffffffffffff16908173ffffffffffffffffffffffffffffffffffffffff168152505060606007604051908082528060200260200182016040528015611b8b5781602001602082028038833980820191505090505b50905061271081600081518110611b9e57fe5b60200260200101818152505061271081600181518110611bba57fe5b60200260200101818152505061271081600281518110611bd657fe5b60200260200101818152505061271081600381518110611bf257fe5b60200260200101818152505061271081600481518110611c0e57fe5b60200260200101818152505061271081600581518110611c2a57fe5b60200260200101818152505061271081600681518110611c4657fe5b60200260200101818152505081819350935050509091565b60ff81565b60016020528160005260406000208181548110611c7c57fe5b9060005260206000209060030201600091509150508060000154908060010154908060020160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff16905083565b6000611cda611cd4611781565b83610939565b9050919050565b604051611ced9061331a565b604051809103902081565b6000606060016000848152602001908152602001600020805480602002602001604051908101604052809291908181526020016000905b82821015611dcb578382906000526020600020906003020160405180606001604052908160008201548152602001600182015481526020016002820160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152505081526020019060010190611d2f565b505050509050600080905060008090505b8251811015611e1e57611e0f838281518110611df457fe5b6020026020010151602001518361239c90919063ffffffff16565b91508080600101915050611ddc565b508092505050919050565b604051611e359061332f565b604051809103902081565b600080600080611e4e611781565b905060026000828152602001908152602001600020600001546002600083815260200190815260200160002060010154600260008481526020019081526020016000206002015493509350935050909192565b60008060038054905090505b6000811115611f6157611ebe612959565b6002600060036001850381548110611ed257fe5b906000526020600020015481526020019081526020016000206040518060600160405290816000820154815260200160018201548152602001600282015481525050905083816020015111158015611f2f57506000816040015114155b8015611f3f575080604001518411155b15611f5257806000015192505050611f9c565b50808060019003915050611ead565b5060006003805490501115611f9757600360016003805490500381548110611f8557fe5b90600052602060002001549050611f9c565b600090505b919050565b606080611fad4361075d565b915091509091565b60038181548110611fc257fe5b906000526020600020016000915090505481565b600281565b60006020528160005260406000208181548110611ff457fe5b9060005260206000209060030201600091509150508060000154908060010154908060020160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff16905083565b60006040438161204b57fe5b04905090565b60026020528060005260406000206000915090508060000154908060010154908060020154905083565b60608061208661189d565b8092508193505050600080905060405180606001604052808281526020016000815260200160ff81525060026000838152602001908152602001600020600082015181600001556020820151816001015560408201518160020155905050600381908060018154018082558091505090600182039060005260206000200160009091929091909150555060008060008381526020019081526020016000208161212f91906128f0565b506000600160008381526020019081526020016000208161215091906128f0565b5060008090505b835181101561227257600080838152602001908152602001600020805480919060010161218491906128f0565b5060405180606001604052808281526020018483815181106121a257fe5b602002602001015181526020018583815181106121bb57fe5b602002602001015173ffffffffffffffffffffffffffffffffffffffff1681525060008084815260200190815260200160002082815481106121f957fe5b9060005260206000209060030201600082015181600001556020820151816001015560408201518160020160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055509050508080600101915050612157565b5060008090505b8351811015612396576001600083815260200190815260200160002080548091906001016122a791906128f0565b5060405180606001604052808281526020018483815181106122c557fe5b602002602001015181526020018583815181106122de57fe5b602002602001015173ffffffffffffffffffffffffffffffffffffffff1681525060016000848152602001908152602001600020828154811061231d57fe5b9060005260206000209060030201600082015181600001556020820151816001015560408201518160020160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055509050508080600101915050612279565b50505050565b6000808284019050838110156123b157600080fd5b8091505092915050565b6123c361297a565b600060208301905060405180604001604052808451815260200182815250915050919050565b60606123f4826126f0565b6123fd57600080fd5b60006124088361273e565b905060608160405190808252806020026020018201604052801561244657816020015b612433612994565b81526020019060019003908161242b5790505b509050600061245885602001516127af565b8560200151019050600080600090505b848110156124b95761247983612838565b915060405180604001604052808381526020018481525084828151811061249c57fe5b602002602001018190525081830192508080600101915050612468565b5082945050505050919050565b60008082600001511180156124e057506021826000015111155b6124e957600080fd5b60006124f883602001516127af565b9050600081846000015103905060008083866020015101905080519150602083101561252b57826020036101000a820491505b81945050505050919050565b6000601582600001511461254a57600080fd5b612553826124c6565b9050919050565b60608183018451101561256c57600080fd5b6060821560008114612589576040519150602082016040526125da565b6040519150601f8416801560200281840101858101878315602002848b0101015b818310156125c757805183526020830192506020810190506125aa565b50868552601f19601f8301166040525050505b50809150509392505050565b600080600080604185511461260157600093505050506126ea565b602085015192506040850151915060ff6041860151169050601b8160ff16101561262c57601b810190505b601b8160ff16141580156126445750601c8160ff1614155b1561265557600093505050506126ea565b60006001878386866040516000815260200160405260405161267a94939291906133e1565b6020604051602081039080840390855afa15801561269c573d6000803e3d6000fd5b505050602060405103519050600073ffffffffffffffffffffffffffffffffffffffff168173ffffffffffffffffffffffffffffffffffffffff1614156126e257600080fd5b809450505050505b92915050565b600080826000015114156127075760009050612739565b60008083602001519050805160001a915060c060ff168260ff16101561273257600092505050612739565b6001925050505b919050565b6000808260000151141561275557600090506127aa565b6000809050600061276984602001516127af565b84602001510190506000846000015185602001510190505b808210156127a35761279282612838565b820191508280600101935050612781565b8293505050505b919050565b600080825160001a9050608060ff168110156127cf576000915050612833565b60b860ff168110806127f4575060c060ff1681101580156127f3575060f860ff1681105b5b15612803576001915050612833565b60c060ff168110156128235760018060b80360ff16820301915050612833565b60018060f80360ff168203019150505b919050565b6000806000835160001a9050608060ff1681101561285957600191506128e6565b60b860ff16811015612876576001608060ff1682030191506128e5565b60c060ff168110156128a65760b78103600185019450806020036101000a855104600182018101935050506128e4565b60f860ff168110156128c357600160c060ff1682030191506128e3565b60f78103600185019450806020036101000a855104600182018101935050505b5b5b5b8192505050919050565b81548183558181111561291d5760030281600302836000526020600020918201910161291c91906129ae565b5b505050565b60405180606001604052806000815260200160008152602001600073ffffffffffffffffffffffffffffffffffffffff1681525090565b60405180606001604052806000815260200160008152602001600081525090565b604051806040016040528060008152602001600081525090565b604051806040016040528060008152602001600081525090565b612a0191905b808211156129fd5760008082016000905560018201600090556002820160006101000a81549073ffffffffffffffffffffffffffffffffffffffff0219169055506003016129b4565b5090565b90565b600081359050612a1381613783565b92915050565b600081359050612a288161379a565b92915050565b600081519050612a3d8161379a565b92915050565b60008083601f840112612a5557600080fd5b8235905067ffffffffffffffff811115612a6e57600080fd5b602083019150836001820283011115612a8657600080fd5b9250929050565b600082601f830112612a9e57600080fd5b8135612ab1612aac826135d2565b6135a5565b91508082526020830160208301858383011115612acd57600080fd5b612ad883828461372d565b50505092915050565b600081359050612af0816137b1565b92915050565b600060208284031215612b0857600080fd5b6000612b1684828501612a04565b91505092915050565b600060208284031215612b3157600080fd5b6000612b3f84828501612a19565b91505092915050565b600060208284031215612b5a57600080fd5b6000612b6884828501612a2e565b91505092915050565b60008060408385031215612b8457600080fd5b6000612b9285828601612a19565b9250506020612ba385828601612a19565b9150509250929050565b600080600060608486031215612bc257600080fd5b6000612bd086828701612a19565b9350506020612be186828701612a19565b925050604084013567ffffffffffffffff811115612bfe57600080fd5b612c0a86828701612a8d565b9150509250925092565b600060208284031215612c2657600080fd5b6000612c3484828501612ae1565b91505092915050565b60008060408385031215612c5057600080fd5b6000612c5e85828601612ae1565b9250506020612c6f85828601612a04565b9150509250929050565b600080600060608486031215612c8e57600080fd5b6000612c9c86828701612ae1565b9350506020612cad86828701612a19565b925050604084013567ffffffffffffffff811115612cca57600080fd5b612cd686828701612a8d565b9150509250925092565b60008060408385031215612cf357600080fd5b6000612d0185828601612ae1565b9250506020612d1285828601612ae1565b9150509250929050565b600080600080600080600060a0888a031215612d3757600080fd5b6000612d458a828b01612ae1565b9750506020612d568a828b01612ae1565b9650506040612d678a828b01612ae1565b955050606088013567ffffffffffffffff811115612d8457600080fd5b612d908a828b01612a43565b9450945050608088013567ffffffffffffffff811115612daf57600080fd5b612dbb8a828b01612a43565b925092505092959891949750929550565b6000612dd88383612dfc565b60208301905092915050565b6000612df0838361326d565b60208301905092915050565b612e05816136a2565b82525050565b612e14816136a2565b82525050565b6000612e258261361e565b612e2f8185613659565b9350612e3a836135fe565b8060005b83811015612e6b578151612e528882612dcc565b9750612e5d8361363f565b925050600181019050612e3e565b5085935050505092915050565b6000612e8382613629565b612e8d818561366a565b9350612e988361360e565b8060005b83811015612ec9578151612eb08882612de4565b9750612ebb8361364c565b925050600181019050612e9c565b5085935050505092915050565b612edf816136b4565b82525050565b612ef6612ef1826136c0565b61376f565b82525050565b612f05816136ec565b82525050565b612f1c612f17826136ec565b613779565b82525050565b6000612f2d82613634565b612f37818561367b565b9350612f4781856020860161373c565b80840191505092915050565b6000612f60600483613697565b91507f766f7465000000000000000000000000000000000000000000000000000000006000830152600482019050919050565b6000612fa0602d83613686565b91507f537461727420626c6f636b206d7573742062652067726561746572207468616e60008301527f2063757272656e74207370616e000000000000000000000000000000000000006020830152604082019050919050565b6000613006600383613697565b91507f31333700000000000000000000000000000000000000000000000000000000006000830152600382019050919050565b6000613046600f83613686565b91507f496e76616c6964207370616e20696400000000000000000000000000000000006000830152602082019050919050565b6000613086601383613686565b91507f5370616e20616c726561647920657869737473000000000000000000000000006000830152602082019050919050565b60006130c6604583613686565b91507f446966666572656e6365206265747765656e20737461727420616e6420656e6460008301527f20626c6f636b206d75737420626520696e206d756c7469706c6573206f66207360208301527f7072696e740000000000000000000000000000000000000000000000000000006040830152606082019050919050565b6000613152600c83613697565b91507f6865696d64616c6c2d31333700000000000000000000000000000000000000006000830152600c82019050919050565b6000613192602a83613686565b91507f456e6420626c6f636b206d7573742062652067726561746572207468616e207360008301527f7461727420626c6f636b000000000000000000000000000000000000000000006020830152604082019050919050565b60006131f8601283613686565b91507f4e6f742053797374656d204164646573732100000000000000000000000000006000830152602082019050919050565b606082016000820151613241600085018261326d565b506020820151613254602085018261326d565b5060408201516132676040850182612dfc565b50505050565b61327681613716565b82525050565b61328581613716565b82525050565b61329481613720565b82525050565b60006132a68285612ee5565b6001820191506132b68284612f0b565b6020820191508190509392505050565b60006132d28286612ee5565b6001820191506132e28285612f0b565b6020820191506132f28284612f0b565b602082019150819050949350505050565b600061330f8284612f22565b915081905092915050565b600061332582612f53565b9150819050919050565b600061333a82612ff9565b9150819050919050565b600061334f82613145565b9150819050919050565b600060208201905061336e6000830184612e0b565b92915050565b6000604082019050818103600083015261338e8185612e1a565b905081810360208301526133a28184612e78565b90509392505050565b60006020820190506133c06000830184612ed6565b92915050565b60006020820190506133db6000830184612efc565b92915050565b60006080820190506133f66000830187612efc565b613403602083018661328b565b6134106040830185612efc565b61341d6060830184612efc565b95945050505050565b6000602082019050818103600083015261343f81612f93565b9050919050565b6000602082019050818103600083015261345f81613039565b9050919050565b6000602082019050818103600083015261347f81613079565b9050919050565b6000602082019050818103600083015261349f816130b9565b9050919050565b600060208201905081810360008301526134bf81613185565b9050919050565b600060208201905081810360008301526134df816131eb565b9050919050565b60006060820190506134fb600083018461322b565b92915050565b6000602082019050613516600083018461327c565b92915050565b6000606082019050613531600083018661327c565b61353e602083018561327c565b61354b6040830184612e0b565b949350505050565b6000606082019050613568600083018661327c565b613575602083018561327c565b613582604083018461327c565b949350505050565b600060208201905061359f600083018461328b565b92915050565b6000604051905081810181811067ffffffffffffffff821117156135c857600080fd5b8060405250919050565b600067ffffffffffffffff8211156135e957600080fd5b601f19601f8301169050602081019050919050565b6000819050602082019050919050565b6000819050602082019050919050565b600081519050919050565b600081519050919050565b600081519050919050565b6000602082019050919050565b6000602082019050919050565b600082825260208201905092915050565b600082825260208201905092915050565b600081905092915050565b600082825260208201905092915050565b600081905092915050565b60006136ad826136f6565b9050919050565b60008115159050919050565b60007fff0000000000000000000000000000000000000000000000000000000000000082169050919050565b6000819050919050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000819050919050565b600060ff82169050919050565b82818337600083830152505050565b60005b8381101561375a57808201518184015260208101905061373f565b83811115613769576000848401525b50505050565b6000819050919050565b6000819050919050565b61378c816136a2565b811461379757600080fd5b50565b6137a3816136ec565b81146137ae57600080fd5b50565b6137ba81613716565b81146137c557600080fd5b5056fea365627a7a72315820638c74b73aaddeb2f2fb9267028e09737291458f6da93b6619d30c86432701d96c6578706572696d656e74616cf564736f6c634300050b0040" - }, - "0000000000000000000000000000000000001001": { - "balance": "0x0", - "code": "0x608060405234801561001057600080fd5b50600436106100415760003560e01c806319494a17146100465780633434735f146100e15780635407ca671461012b575b600080fd5b6100c76004803603604081101561005c57600080fd5b81019080803590602001909291908035906020019064010000000081111561008357600080fd5b82018360208201111561009557600080fd5b803590602001918460018302840111640100000000831117156100b757600080fd5b9091929391929390505050610149565b604051808215151515815260200191505060405180910390f35b6100e961047a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b610133610492565b6040518082815260200191505060405180910390f35b600073fffffffffffffffffffffffffffffffffffffffe73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614610200576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260128152602001807f4e6f742053797374656d2041646465737321000000000000000000000000000081525060200191505060405180910390fd5b606061025761025285858080601f016020809104026020016040519081016040528093929190818152602001838380828437600081840152601f19601f82011690508083019250505050505050610498565b6104c6565b905060006102788260008151811061026b57fe5b60200260200101516105a3565b905080600160005401146102f4576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252601b8152602001807f537461746549647320617265206e6f742073657175656e7469616c000000000081525060200191505060405180910390fd5b600080815480929190600101919050555060006103248360018151811061031757fe5b6020026020010151610614565b905060606103458460028151811061033857fe5b6020026020010151610637565b9050610350826106c3565b1561046f576000624c4b409050606084836040516024018083815260200180602001828103825283818151815260200191508051906020019080838360005b838110156103aa57808201518184015260208101905061038f565b50505050905090810190601f1680156103d75780820380516001836020036101000a031916815260200191505b5093505050506040516020818303038152906040527f26c53bea000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff8381831617835250505050905060008082516020840160008887f1965050505b505050509392505050565b73fffffffffffffffffffffffffffffffffffffffe81565b60005481565b6104a0610943565b600060208301905060405180604001604052808451815260200182815250915050919050565b60606104d1826106dc565b6104da57600080fd5b60006104e58361072a565b905060608160405190808252806020026020018201604052801561052357816020015b61051061095d565b8152602001906001900390816105085790505b5090506000610535856020015161079b565b8560200151019050600080600090505b848110156105965761055683610824565b915060405180604001604052808381526020018481525084828151811061057957fe5b602002602001018190525081830192508080600101915050610545565b5082945050505050919050565b60008082600001511180156105bd57506021826000015111155b6105c657600080fd5b60006105d5836020015161079b565b9050600081846000015103905060008083866020015101905080519150602083101561060857826020036101000a820491505b81945050505050919050565b6000601582600001511461062757600080fd5b610630826105a3565b9050919050565b6060600082600001511161064a57600080fd5b6000610659836020015161079b565b905060008184600001510390506060816040519080825280601f01601f19166020018201604052801561069b5781602001600182028038833980820191505090505b50905060008160200190506106b78487602001510182856108dc565b81945050505050919050565b600080823b905060008163ffffffff1611915050919050565b600080826000015114156106f35760009050610725565b60008083602001519050805160001a915060c060ff168260ff16101561071e57600092505050610725565b6001925050505b919050565b600080826000015114156107415760009050610796565b60008090506000610755846020015161079b565b84602001510190506000846000015185602001510190505b8082101561078f5761077e82610824565b82019150828060010193505061076d565b8293505050505b919050565b600080825160001a9050608060ff168110156107bb57600091505061081f565b60b860ff168110806107e0575060c060ff1681101580156107df575060f860ff1681105b5b156107ef57600191505061081f565b60c060ff1681101561080f5760018060b80360ff1682030191505061081f565b60018060f80360ff168203019150505b919050565b6000806000835160001a9050608060ff1681101561084557600191506108d2565b60b860ff16811015610862576001608060ff1682030191506108d1565b60c060ff168110156108925760b78103600185019450806020036101000a855104600182018101935050506108d0565b60f860ff168110156108af57600160c060ff1682030191506108cf565b60f78103600185019450806020036101000a855104600182018101935050505b5b5b5b8192505050919050565b60008114156108ea5761093e565b5b602060ff16811061091a5782518252602060ff1683019250602060ff1682019150602060ff16810390506108eb565b6000600182602060ff16036101000a03905080198451168184511681811785525050505b505050565b604051806040016040528060008152602001600081525090565b60405180604001604052806000815260200160008152509056fea265627a7a7231582083fbdacb76f32b4112d0f7db9a596937925824798a0026ba0232322390b5263764736f6c634300050b0032" - }, - "0000000000000000000000000000000000001010": { - "balance": "0x204fcce2c5a141f7f9a00000", - "code": "0x60806040526004361061019c5760003560e01c806377d32e94116100ec578063acd06cb31161008a578063e306f77911610064578063e306f77914610a7b578063e614d0d614610aa6578063f2fde38b14610ad1578063fc0c546a14610b225761019c565b8063acd06cb31461097a578063b789543c146109cd578063cc79f97b14610a505761019c565b80639025e64c116100c65780639025e64c146107c957806395d89b4114610859578063a9059cbb146108e9578063abceeba21461094f5761019c565b806377d32e94146106315780638da5cb5b146107435780638f32d59b1461079a5761019c565b806347e7ef24116101595780637019d41a116101335780637019d41a1461053357806370a082311461058a578063715018a6146105ef578063771282f6146106065761019c565b806347e7ef2414610410578063485cc9551461046b57806360f96a8f146104dc5761019c565b806306fdde03146101a15780631499c5921461023157806318160ddd1461028257806319d27d9c146102ad5780632e1a7d4d146103b1578063313ce567146103df575b600080fd5b3480156101ad57600080fd5b506101b6610b79565b6040518080602001828103825283818151815260200191508051906020019080838360005b838110156101f65780820151818401526020810190506101db565b50505050905090810190601f1680156102235780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b34801561023d57600080fd5b506102806004803603602081101561025457600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff169060200190929190505050610bb6565b005b34801561028e57600080fd5b50610297610c24565b6040518082815260200191505060405180910390f35b3480156102b957600080fd5b5061036f600480360360a08110156102d057600080fd5b81019080803590602001906401000000008111156102ed57600080fd5b8201836020820111156102ff57600080fd5b8035906020019184600183028401116401000000008311171561032157600080fd5b9091929391929390803590602001909291908035906020019092919080359060200190929190803573ffffffffffffffffffffffffffffffffffffffff169060200190929190505050610c3a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b6103dd600480360360208110156103c757600080fd5b8101908080359060200190929190505050610e06565b005b3480156103eb57600080fd5b506103f4610f58565b604051808260ff1660ff16815260200191505060405180910390f35b34801561041c57600080fd5b506104696004803603604081101561043357600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff16906020019092919080359060200190929190505050610f61565b005b34801561047757600080fd5b506104da6004803603604081101561048e57600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff169060200190929190803573ffffffffffffffffffffffffffffffffffffffff16906020019092919050505061111d565b005b3480156104e857600080fd5b506104f16111ec565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b34801561053f57600080fd5b50610548611212565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b34801561059657600080fd5b506105d9600480360360208110156105ad57600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff169060200190929190505050611238565b6040518082815260200191505060405180910390f35b3480156105fb57600080fd5b50610604611259565b005b34801561061257600080fd5b5061061b611329565b6040518082815260200191505060405180910390f35b34801561063d57600080fd5b506107016004803603604081101561065457600080fd5b81019080803590602001909291908035906020019064010000000081111561067b57600080fd5b82018360208201111561068d57600080fd5b803590602001918460018302840111640100000000831117156106af57600080fd5b91908080601f016020809104026020016040519081016040528093929190818152602001838380828437600081840152601f19601f82011690508083019250505050505050919291929050505061132f565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b34801561074f57600080fd5b506107586114b4565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b3480156107a657600080fd5b506107af6114dd565b604051808215151515815260200191505060405180910390f35b3480156107d557600080fd5b506107de611534565b6040518080602001828103825283818151815260200191508051906020019080838360005b8381101561081e578082015181840152602081019050610803565b50505050905090810190601f16801561084b5780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b34801561086557600080fd5b5061086e61156d565b6040518080602001828103825283818151815260200191508051906020019080838360005b838110156108ae578082015181840152602081019050610893565b50505050905090810190601f1680156108db5780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b610935600480360360408110156108ff57600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff169060200190929190803590602001909291905050506115aa565b604051808215151515815260200191505060405180910390f35b34801561095b57600080fd5b506109646115d0565b6040518082815260200191505060405180910390f35b34801561098657600080fd5b506109b36004803603602081101561099d57600080fd5b810190808035906020019092919050505061165d565b604051808215151515815260200191505060405180910390f35b3480156109d957600080fd5b50610a3a600480360360808110156109f057600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff16906020019092919080359060200190929190803590602001909291908035906020019092919050505061167d565b6040518082815260200191505060405180910390f35b348015610a5c57600080fd5b50610a6561169d565b6040518082815260200191505060405180910390f35b348015610a8757600080fd5b50610a906116a2565b6040518082815260200191505060405180910390f35b348015610ab257600080fd5b50610abb6116a8565b6040518082815260200191505060405180910390f35b348015610add57600080fd5b50610b2060048036036020811015610af457600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff169060200190929190505050611735565b005b348015610b2e57600080fd5b50610b37611752565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b60606040518060400160405280600b81526020017f4d6174696320546f6b656e000000000000000000000000000000000000000000815250905090565b6040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260108152602001807f44697361626c656420666561747572650000000000000000000000000000000081525060200191505060405180910390fd5b6000601260ff16600a0a6402540be40002905090565b6000808511610c4857600080fd5b6000831480610c575750824311155b610cc9576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260148152602001807f5369676e6174757265206973206578706972656400000000000000000000000081525060200191505060405180910390fd5b6000610cd73387878761167d565b9050600015156005600083815260200190815260200160002060009054906101000a900460ff16151514610d73576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252600f8152602001807f536967206465616374697661746564000000000000000000000000000000000081525060200191505060405180910390fd5b60016005600083815260200190815260200160002060006101000a81548160ff021916908315150217905550610ded8189898080601f016020809104026020016040519081016040528093929190818152602001838380828437600081840152601f19601f8201169050808301925050505050505061132f565b9150610dfa828488611778565b50509695505050505050565b60003390506000610e1682611238565b9050610e2d83600654611b3590919063ffffffff16565b600681905550600083118015610e4257508234145b610eb4576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260138152602001807f496e73756666696369656e7420616d6f756e740000000000000000000000000081525060200191505060405180910390fd5b8173ffffffffffffffffffffffffffffffffffffffff16600260009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff167febff2602b3f468259e1e99f613fed6691f3a6526effe6ef3e768ba7ae7a36c4f8584610f3087611238565b60405180848152602001838152602001828152602001935050505060405180910390a3505050565b60006012905090565b610f696114dd565b610f7257600080fd5b600081118015610faf5750600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff1614155b611004576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526023815260200180611f036023913960400191505060405180910390fd5b600061100f83611238565b905060008390508073ffffffffffffffffffffffffffffffffffffffff166108fc849081150290604051600060405180830381858888f1935050505015801561105c573d6000803e3d6000fd5b5061107283600654611b5590919063ffffffff16565b6006819055508373ffffffffffffffffffffffffffffffffffffffff16600260009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff167f4e2ca0515ed1aef1395f66b5303bb5d6f1bf9d61a353fa53f73f8ac9973fa9f685856110f489611238565b60405180848152602001838152602001828152602001935050505060405180910390a350505050565b600760009054906101000a900460ff1615611183576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526023815260200180611ee06023913960400191505060405180910390fd5b6001600760006101000a81548160ff02191690831515021790555080600260006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506111e882611b74565b5050565b600360009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b600460009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b60008173ffffffffffffffffffffffffffffffffffffffff16319050919050565b6112616114dd565b61126a57600080fd5b600073ffffffffffffffffffffffffffffffffffffffff166000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff167f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e060405160405180910390a360008060006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550565b60065481565b600080600080604185511461134a57600093505050506114ae565b602085015192506040850151915060ff6041860151169050601b8160ff16101561137557601b810190505b601b8160ff161415801561138d5750601c8160ff1614155b1561139e57600093505050506114ae565b60018682858560405160008152602001604052604051808581526020018460ff1660ff1681526020018381526020018281526020019450505050506020604051602081039080840390855afa1580156113fb573d6000803e3d6000fd5b505050602060405103519350600073ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff1614156114aa576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260128152602001807f4572726f7220696e2065637265636f766572000000000000000000000000000081525060200191505060405180910390fd5b5050505b92915050565b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff16905090565b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614905090565b6040518060400160405280600181526020017f890000000000000000000000000000000000000000000000000000000000000081525081565b60606040518060400160405280600581526020017f4d41544943000000000000000000000000000000000000000000000000000000815250905090565b60008134146115bc57600090506115ca565b6115c7338484611778565b90505b92915050565b6040518060800160405280605b8152602001611f78605b91396040516020018082805190602001908083835b6020831061161f57805182526020820191506020810190506020830392506115fc565b6001836020036101000a0380198251168184511680821785525050505050509050019150506040516020818303038152906040528051906020012081565b60056020528060005260406000206000915054906101000a900460ff1681565b600061169361168e86868686611c6c565b611d42565b9050949350505050565b608981565b60015481565b604051806080016040528060528152602001611f26605291396040516020018082805190602001908083835b602083106116f757805182526020820191506020810190506020830392506116d4565b6001836020036101000a0380198251168184511680821785525050505050509050019150506040516020818303038152906040528051906020012081565b61173d6114dd565b61174657600080fd5b61174f81611b74565b50565b600260009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000803073ffffffffffffffffffffffffffffffffffffffff166370a08231866040518263ffffffff1660e01b8152600401808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060206040518083038186803b1580156117f857600080fd5b505afa15801561180c573d6000803e3d6000fd5b505050506040513d602081101561182257600080fd5b8101908080519060200190929190505050905060003073ffffffffffffffffffffffffffffffffffffffff166370a08231866040518263ffffffff1660e01b8152600401808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060206040518083038186803b1580156118b457600080fd5b505afa1580156118c8573d6000803e3d6000fd5b505050506040513d60208110156118de57600080fd5b810190808051906020019092919050505090506118fc868686611d8c565b8473ffffffffffffffffffffffffffffffffffffffff168673ffffffffffffffffffffffffffffffffffffffff16600260009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff167fe6497e3ee548a3372136af2fcb0696db31fc6cf20260707645068bd3fe97f3c48786863073ffffffffffffffffffffffffffffffffffffffff166370a082318e6040518263ffffffff1660e01b8152600401808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060206040518083038186803b158015611a0457600080fd5b505afa158015611a18573d6000803e3d6000fd5b505050506040513d6020811015611a2e57600080fd5b81019080805190602001909291905050503073ffffffffffffffffffffffffffffffffffffffff166370a082318e6040518263ffffffff1660e01b8152600401808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060206040518083038186803b158015611abc57600080fd5b505afa158015611ad0573d6000803e3d6000fd5b505050506040513d6020811015611ae657600080fd5b8101908080519060200190929190505050604051808681526020018581526020018481526020018381526020018281526020019550505050505060405180910390a46001925050509392505050565b600082821115611b4457600080fd5b600082840390508091505092915050565b600080828401905083811015611b6a57600080fd5b8091505092915050565b600073ffffffffffffffffffffffffffffffffffffffff168173ffffffffffffffffffffffffffffffffffffffff161415611bae57600080fd5b8073ffffffffffffffffffffffffffffffffffffffff166000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff167f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e060405160405180910390a3806000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555050565b6000806040518060800160405280605b8152602001611f78605b91396040516020018082805190602001908083835b60208310611cbe5780518252602082019150602081019050602083039250611c9b565b6001836020036101000a03801982511681845116808217855250505050505090500191505060405160208183030381529060405280519060200120905060405181815273ffffffffffffffffffffffffffffffffffffffff8716602082015285604082015284606082015283608082015260a0812092505081915050949350505050565b60008060015490506040517f190100000000000000000000000000000000000000000000000000000000000081528160028201528360228201526042812092505081915050919050565b3073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff161415611e2e576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260138152602001807f63616e27742073656e6420746f204d524332300000000000000000000000000081525060200191505060405180910390fd5b8173ffffffffffffffffffffffffffffffffffffffff166108fc829081150290604051600060405180830381858888f19350505050158015611e74573d6000803e3d6000fd5b508173ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef836040518082815260200191505060405180910390a350505056fe54686520636f6e747261637420697320616c726561647920696e697469616c697a6564496e73756666696369656e7420616d6f756e74206f7220696e76616c69642075736572454950373132446f6d61696e28737472696e67206e616d652c737472696e672076657273696f6e2c75696e7432353620636861696e49642c6164647265737320766572696679696e67436f6e747261637429546f6b656e5472616e736665724f726465722861646472657373207370656e6465722c75696e7432353620746f6b656e49644f72416d6f756e742c6279746573333220646174612c75696e743235362065787069726174696f6e29a265627a7a7231582098247ec3c8d127ebf969c8f317e340b1cd6c481af077234c38e0c7d92aba4d6364736f6c634300050b0032" - }, - "5973918275C01F50555d44e92c9d9b353CaDAD54": { - "balance": "0x3635c9adc5dea00000" - }, - "b8bB158B93c94ed35c1970D610d1E2B34E26652c": { - "balance": "0x3635c9adc5dea00000" - }, - "F84C74dEa96DF0EC22e11e7C33996C73FCC2D822": { - "balance": "0x3635c9adc5dea00000" - }, - "b702f1C9154ac9c08Da247a8e30ee6F2F3373f41": { - "balance": "0x3635c9adc5dea00000" - }, - "7fCD58C2D53D980b247F1612FdbA93E9a76193E6": { - "balance": "0x3635c9adc5dea00000" - }, - "0375b2fc7140977c9c76D45421564e354ED42277": { - "balance": "0x3635c9adc5dea00000" - }, - "42EEfcda06eaD475cdE3731B8eb138e88CD0bAC3": { - "balance": "0x3635c9adc5dea00000" - } - }, - "number": "0x0", - "gasUsed": "0x0", - "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000" -} diff --git a/builder/files/genesis-testnet-v4.json b/builder/files/genesis-testnet-v4.json deleted file mode 100644 index 34407a391a..0000000000 --- a/builder/files/genesis-testnet-v4.json +++ /dev/null @@ -1,96 +0,0 @@ -{ - "config": { - "chainId": 80001, - "homesteadBlock": 0, - "eip150Block": 0, - "eip150Hash": "0x0000000000000000000000000000000000000000000000000000000000000000", - "eip155Block": 0, - "eip158Block": 0, - "byzantiumBlock": 0, - "constantinopleBlock": 0, - "petersburgBlock": 0, - "istanbulBlock": 2722000, - "muirGlacierBlock": 2722000, - "berlinBlock": 13996000, - "londonBlock": 22640000, - "bor": { - "jaipurBlock": 22770000, - "delhiBlock": 29638656, - "parallelUniverseBlock": 0, - "indoreBlock": 37075456, - "stateSyncConfirmationDelay": { - "37075456": 128 - }, - "period": { - "0": 2, - "25275000": 5, - "29638656": 2 - }, - "producerDelay": { - "0": 6, - "29638656": 4 - }, - "sprint": { - "0": 64, - "29638656": 16 - }, - "backupMultiplier": { - "0": 2, - "25275000": 5, - "29638656": 2 - }, - "validatorContract": "0x0000000000000000000000000000000000001000", - "stateReceiverContract": "0x0000000000000000000000000000000000001001", - "burntContract": { - "22640000": "0x70bcA57F4579f58670aB2d18Ef16e02C17553C38" - }, - "blockAlloc": { - "22244000": { - "0000000000000000000000000000000000001010": { - "balance": "0x0", - "code": "0x60806040526004361061019c5760003560e01c806377d32e94116100ec578063acd06cb31161008a578063e306f77911610064578063e306f77914610a7b578063e614d0d614610aa6578063f2fde38b14610ad1578063fc0c546a14610b225761019c565b8063acd06cb31461097a578063b789543c146109cd578063cc79f97b14610a505761019c565b80639025e64c116100c65780639025e64c146107c957806395d89b4114610859578063a9059cbb146108e9578063abceeba21461094f5761019c565b806377d32e94146106315780638da5cb5b146107435780638f32d59b1461079a5761019c565b806347e7ef24116101595780637019d41a116101335780637019d41a1461053357806370a082311461058a578063715018a6146105ef578063771282f6146106065761019c565b806347e7ef2414610410578063485cc9551461046b57806360f96a8f146104dc5761019c565b806306fdde03146101a15780631499c5921461023157806318160ddd1461028257806319d27d9c146102ad5780632e1a7d4d146103b1578063313ce567146103df575b600080fd5b3480156101ad57600080fd5b506101b6610b79565b6040518080602001828103825283818151815260200191508051906020019080838360005b838110156101f65780820151818401526020810190506101db565b50505050905090810190601f1680156102235780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b34801561023d57600080fd5b506102806004803603602081101561025457600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff169060200190929190505050610bb6565b005b34801561028e57600080fd5b50610297610c24565b6040518082815260200191505060405180910390f35b3480156102b957600080fd5b5061036f600480360360a08110156102d057600080fd5b81019080803590602001906401000000008111156102ed57600080fd5b8201836020820111156102ff57600080fd5b8035906020019184600183028401116401000000008311171561032157600080fd5b9091929391929390803590602001909291908035906020019092919080359060200190929190803573ffffffffffffffffffffffffffffffffffffffff169060200190929190505050610c3a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b6103dd600480360360208110156103c757600080fd5b8101908080359060200190929190505050610caa565b005b3480156103eb57600080fd5b506103f4610dfc565b604051808260ff1660ff16815260200191505060405180910390f35b34801561041c57600080fd5b506104696004803603604081101561043357600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff16906020019092919080359060200190929190505050610e05565b005b34801561047757600080fd5b506104da6004803603604081101561048e57600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff169060200190929190803573ffffffffffffffffffffffffffffffffffffffff169060200190929190505050610fc1565b005b3480156104e857600080fd5b506104f1611090565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b34801561053f57600080fd5b506105486110b6565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b34801561059657600080fd5b506105d9600480360360208110156105ad57600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff1690602001909291905050506110dc565b6040518082815260200191505060405180910390f35b3480156105fb57600080fd5b506106046110fd565b005b34801561061257600080fd5b5061061b6111cd565b6040518082815260200191505060405180910390f35b34801561063d57600080fd5b506107016004803603604081101561065457600080fd5b81019080803590602001909291908035906020019064010000000081111561067b57600080fd5b82018360208201111561068d57600080fd5b803590602001918460018302840111640100000000831117156106af57600080fd5b91908080601f016020809104026020016040519081016040528093929190818152602001838380828437600081840152601f19601f8201169050808301925050505050505091929192905050506111d3565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b34801561074f57600080fd5b50610758611358565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b3480156107a657600080fd5b506107af611381565b604051808215151515815260200191505060405180910390f35b3480156107d557600080fd5b506107de6113d8565b6040518080602001828103825283818151815260200191508051906020019080838360005b8381101561081e578082015181840152602081019050610803565b50505050905090810190601f16801561084b5780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b34801561086557600080fd5b5061086e611411565b6040518080602001828103825283818151815260200191508051906020019080838360005b838110156108ae578082015181840152602081019050610893565b50505050905090810190601f1680156108db5780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b610935600480360360408110156108ff57600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff1690602001909291908035906020019092919050505061144e565b604051808215151515815260200191505060405180910390f35b34801561095b57600080fd5b50610964611474565b6040518082815260200191505060405180910390f35b34801561098657600080fd5b506109b36004803603602081101561099d57600080fd5b8101908080359060200190929190505050611501565b604051808215151515815260200191505060405180910390f35b3480156109d957600080fd5b50610a3a600480360360808110156109f057600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff169060200190929190803590602001909291908035906020019092919080359060200190929190505050611521565b6040518082815260200191505060405180910390f35b348015610a5c57600080fd5b50610a65611541565b6040518082815260200191505060405180910390f35b348015610a8757600080fd5b50610a90611548565b6040518082815260200191505060405180910390f35b348015610ab257600080fd5b50610abb61154e565b6040518082815260200191505060405180910390f35b348015610add57600080fd5b50610b2060048036036020811015610af457600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff1690602001909291905050506115db565b005b348015610b2e57600080fd5b50610b376115f8565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b60606040518060400160405280600b81526020017f4d6174696320546f6b656e000000000000000000000000000000000000000000815250905090565b6040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260108152602001807f44697361626c656420666561747572650000000000000000000000000000000081525060200191505060405180910390fd5b6000601260ff16600a0a6402540be40002905090565b60006040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260108152602001807f44697361626c656420666561747572650000000000000000000000000000000081525060200191505060405180910390fd5b60003390506000610cba826110dc565b9050610cd18360065461161e90919063ffffffff16565b600681905550600083118015610ce657508234145b610d58576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260138152602001807f496e73756666696369656e7420616d6f756e740000000000000000000000000081525060200191505060405180910390fd5b8173ffffffffffffffffffffffffffffffffffffffff16600260009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff167febff2602b3f468259e1e99f613fed6691f3a6526effe6ef3e768ba7ae7a36c4f8584610dd4876110dc565b60405180848152602001838152602001828152602001935050505060405180910390a3505050565b60006012905090565b610e0d611381565b610e1657600080fd5b600081118015610e535750600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff1614155b610ea8576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526023815260200180611da96023913960400191505060405180910390fd5b6000610eb3836110dc565b905060008390508073ffffffffffffffffffffffffffffffffffffffff166108fc849081150290604051600060405180830381858888f19350505050158015610f00573d6000803e3d6000fd5b50610f168360065461163e90919063ffffffff16565b6006819055508373ffffffffffffffffffffffffffffffffffffffff16600260009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff167f4e2ca0515ed1aef1395f66b5303bb5d6f1bf9d61a353fa53f73f8ac9973fa9f68585610f98896110dc565b60405180848152602001838152602001828152602001935050505060405180910390a350505050565b600760009054906101000a900460ff1615611027576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526023815260200180611d866023913960400191505060405180910390fd5b6001600760006101000a81548160ff02191690831515021790555080600260006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555061108c8261165d565b5050565b600360009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b600460009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b60008173ffffffffffffffffffffffffffffffffffffffff16319050919050565b611105611381565b61110e57600080fd5b600073ffffffffffffffffffffffffffffffffffffffff166000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff167f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e060405160405180910390a360008060006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550565b60065481565b60008060008060418551146111ee5760009350505050611352565b602085015192506040850151915060ff6041860151169050601b8160ff16101561121957601b810190505b601b8160ff16141580156112315750601c8160ff1614155b156112425760009350505050611352565b60018682858560405160008152602001604052604051808581526020018460ff1660ff1681526020018381526020018281526020019450505050506020604051602081039080840390855afa15801561129f573d6000803e3d6000fd5b505050602060405103519350600073ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff16141561134e576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260128152602001807f4572726f7220696e2065637265636f766572000000000000000000000000000081525060200191505060405180910390fd5b5050505b92915050565b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff16905090565b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614905090565b6040518060400160405280600381526020017f013881000000000000000000000000000000000000000000000000000000000081525081565b60606040518060400160405280600581526020017f4d41544943000000000000000000000000000000000000000000000000000000815250905090565b6000813414611460576000905061146e565b61146b338484611755565b90505b92915050565b6040518060800160405280605b8152602001611e1e605b91396040516020018082805190602001908083835b602083106114c357805182526020820191506020810190506020830392506114a0565b6001836020036101000a0380198251168184511680821785525050505050509050019150506040516020818303038152906040528051906020012081565b60056020528060005260406000206000915054906101000a900460ff1681565b600061153761153286868686611b12565b611be8565b9050949350505050565b6201388181565b60015481565b604051806080016040528060528152602001611dcc605291396040516020018082805190602001908083835b6020831061159d578051825260208201915060208101905060208303925061157a565b6001836020036101000a0380198251168184511680821785525050505050509050019150506040516020818303038152906040528051906020012081565b6115e3611381565b6115ec57600080fd5b6115f58161165d565b50565b600260009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b60008282111561162d57600080fd5b600082840390508091505092915050565b60008082840190508381101561165357600080fd5b8091505092915050565b600073ffffffffffffffffffffffffffffffffffffffff168173ffffffffffffffffffffffffffffffffffffffff16141561169757600080fd5b8073ffffffffffffffffffffffffffffffffffffffff166000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff167f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e060405160405180910390a3806000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555050565b6000803073ffffffffffffffffffffffffffffffffffffffff166370a08231866040518263ffffffff1660e01b8152600401808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060206040518083038186803b1580156117d557600080fd5b505afa1580156117e9573d6000803e3d6000fd5b505050506040513d60208110156117ff57600080fd5b8101908080519060200190929190505050905060003073ffffffffffffffffffffffffffffffffffffffff166370a08231866040518263ffffffff1660e01b8152600401808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060206040518083038186803b15801561189157600080fd5b505afa1580156118a5573d6000803e3d6000fd5b505050506040513d60208110156118bb57600080fd5b810190808051906020019092919050505090506118d9868686611c32565b8473ffffffffffffffffffffffffffffffffffffffff168673ffffffffffffffffffffffffffffffffffffffff16600260009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff167fe6497e3ee548a3372136af2fcb0696db31fc6cf20260707645068bd3fe97f3c48786863073ffffffffffffffffffffffffffffffffffffffff166370a082318e6040518263ffffffff1660e01b8152600401808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060206040518083038186803b1580156119e157600080fd5b505afa1580156119f5573d6000803e3d6000fd5b505050506040513d6020811015611a0b57600080fd5b81019080805190602001909291905050503073ffffffffffffffffffffffffffffffffffffffff166370a082318e6040518263ffffffff1660e01b8152600401808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060206040518083038186803b158015611a9957600080fd5b505afa158015611aad573d6000803e3d6000fd5b505050506040513d6020811015611ac357600080fd5b8101908080519060200190929190505050604051808681526020018581526020018481526020018381526020018281526020019550505050505060405180910390a46001925050509392505050565b6000806040518060800160405280605b8152602001611e1e605b91396040516020018082805190602001908083835b60208310611b645780518252602082019150602081019050602083039250611b41565b6001836020036101000a03801982511681845116808217855250505050505090500191505060405160208183030381529060405280519060200120905060405181815273ffffffffffffffffffffffffffffffffffffffff8716602082015285604082015284606082015283608082015260a0812092505081915050949350505050565b60008060015490506040517f190100000000000000000000000000000000000000000000000000000000000081528160028201528360228201526042812092505081915050919050565b3073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff161415611cd4576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260138152602001807f63616e27742073656e6420746f204d524332300000000000000000000000000081525060200191505060405180910390fd5b8173ffffffffffffffffffffffffffffffffffffffff166108fc829081150290604051600060405180830381858888f19350505050158015611d1a573d6000803e3d6000fd5b508173ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef836040518082815260200191505060405180910390a350505056fe54686520636f6e747261637420697320616c726561647920696e697469616c697a6564496e73756666696369656e7420616d6f756e74206f7220696e76616c69642075736572454950373132446f6d61696e28737472696e67206e616d652c737472696e672076657273696f6e2c75696e7432353620636861696e49642c6164647265737320766572696679696e67436f6e747261637429546f6b656e5472616e736665724f726465722861646472657373207370656e6465722c75696e7432353620746f6b656e49644f72416d6f756e742c6279746573333220646174612c75696e743235362065787069726174696f6e29a265627a7a72315820ccd6c2a9c259832bbb367986ee06cd87af23022681b0cb22311a864b701d939564736f6c63430005100032" - } - } - } - } - }, - "nonce": "0x0", - "timestamp": "0x5ce28211", - "extraData": "", - "gasLimit": "0x989680", - "difficulty": "0x1", - "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", - "coinbase": "0x0000000000000000000000000000000000000000", - "alloc": { - "0000000000000000000000000000000000001000": { - "balance": "0x0", - "code": "0x608060405234801561001057600080fd5b50600436106101f05760003560e01c806360c8614d1161010f578063af26aa96116100a2578063d5b844eb11610071578063d5b844eb14610666578063dcf2793a14610684578063e3b7c924146106b6578063f59cf565146106d4576101f0565b8063af26aa96146105c7578063b71d7a69146105e7578063b7ab4db514610617578063c1b3c91914610636576101f0565b806370ba5707116100de57806370ba57071461052b57806398ab2b621461055b5780639d11b80714610579578063ae756451146105a9576101f0565b806360c8614d1461049c57806365b3a1e2146104bc57806366332354146104db578063687a9bd6146104f9576101f0565b80633434735f1161018757806344d6528f1161015657806344d6528f146103ee5780634dbc959f1461041e57806355614fcc1461043c578063582a8d081461046c576101f0565b80633434735f1461035257806335ddfeea1461037057806343ee8213146103a057806344c15cb1146103be576101f0565b806323f2a73f116101c357806323f2a73f146102a45780632bc06564146102d45780632de3a180146102f25780632eddf35214610322576101f0565b8063047a6c5b146101f55780630c35b1cb146102275780631270b5741461025857806323c2a2b414610288575b600080fd5b61020f600480360361020a9190810190612b24565b610706565b60405161021e93929190613463565b60405180910390f35b610241600480360361023c9190810190612b24565b61075d565b60405161024f929190613284565b60405180910390f35b610272600480360361026d9190810190612b4d565b610939565b60405161027f91906132bb565b60405180910390f35b6102a2600480360361029d9190810190612c2c565b610a91565b005b6102be60048036036102b99190810190612b4d565b61112a565b6040516102cb91906132bb565b60405180910390f35b6102dc611281565b6040516102e99190613411565b60405180910390f35b61030c60048036036103079190810190612a81565b611286565b60405161031991906132d6565b60405180910390f35b61033c60048036036103379190810190612b24565b611307565b6040516103499190613411565b60405180910390f35b61035a611437565b6040516103679190613269565b60405180910390f35b61038a60048036036103859190810190612abd565b61144f565b60405161039791906132bb565b60405180910390f35b6103a861151a565b6040516103b591906132d6565b60405180910390f35b6103d860048036036103d39190810190612b89565b611531565b6040516103e59190613411565b60405180910390f35b61040860048036036104039190810190612b4d565b611619565b60405161041591906133f6565b60405180910390f35b610426611781565b6040516104339190613411565b60405180910390f35b61045660048036036104519190810190612a06565b611791565b60405161046391906132bb565b60405180910390f35b61048660048036036104819190810190612a2f565b6117ab565b60405161049391906132d6565b60405180910390f35b6104a4611829565b6040516104b393929190613463565b60405180910390f35b6104c461189d565b6040516104d2929190613284565b60405180910390f35b6104e3611b6e565b6040516104f09190613411565b60405180910390f35b610513600480360361050e9190810190612bf0565b611b73565b6040516105229392919061342c565b60405180910390f35b61054560048036036105409190810190612a06565b611bd7565b60405161055291906132bb565b60405180910390f35b610563611bf1565b60405161057091906132d6565b60405180910390f35b610593600480360361058e9190810190612b24565b611c08565b6040516105a09190613411565b60405180910390f35b6105b1611d39565b6040516105be91906132d6565b60405180910390f35b6105cf611d50565b6040516105de93929190613463565b60405180910390f35b61060160048036036105fc9190810190612b24565b611db1565b60405161060e9190613411565b60405180910390f35b61061f611eb1565b60405161062d929190613284565b60405180910390f35b610650600480360361064b9190810190612b24565b611ec5565b60405161065d9190613411565b60405180910390f35b61066e611ee6565b60405161067b919061349a565b60405180910390f35b61069e60048036036106999190810190612bf0565b611eeb565b6040516106ad9392919061342c565b60405180910390f35b6106be611f4f565b6040516106cb9190613411565b60405180910390f35b6106ee60048036036106e99190810190612b24565b611f61565b6040516106fd93929190613463565b60405180910390f35b60008060006002600085815260200190815260200160002060000154600260008681526020019081526020016000206001015460026000878152602001908152602001600020600201549250925092509193909250565b60608060ff83116107795761077061189d565b91509150610934565b600061078484611db1565b9050606060016000838152602001908152602001600020805490506040519080825280602002602001820160405280156107cd5781602001602082028038833980820191505090505b509050606060016000848152602001908152602001600020805490506040519080825280602002602001820160405280156108175781602001602082028038833980820191505090505b50905060008090505b60016000858152602001908152602001600020805490508110156109295760016000858152602001908152602001600020818154811061085c57fe5b906000526020600020906003020160020160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1683828151811061089a57fe5b602002602001019073ffffffffffffffffffffffffffffffffffffffff16908173ffffffffffffffffffffffffffffffffffffffff16815250506001600085815260200190815260200160002081815481106108f257fe5b90600052602060002090600302016001015482828151811061091057fe5b6020026020010181815250508080600101915050610820565b508181945094505050505b915091565b6000606060016000858152602001908152602001600020805480602002602001604051908101604052809291908181526020016000905b82821015610a0c578382906000526020600020906003020160405180606001604052908160008201548152602001600182015481526020016002820160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152505081526020019060010190610970565b50505050905060008090505b8151811015610a84578373ffffffffffffffffffffffffffffffffffffffff16828281518110610a4457fe5b60200260200101516040015173ffffffffffffffffffffffffffffffffffffffff161415610a7757600192505050610a8b565b8080600101915050610a18565b5060009150505b92915050565b73fffffffffffffffffffffffffffffffffffffffe73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614610b13576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610b0a906133d6565b60405180910390fd5b6000610b1d611781565b90506000811415610b3157610b30611f8b565b5b610b456001826122ac90919063ffffffff16565b8814610b86576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610b7d90613356565b60405180910390fd5b868611610bc8576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610bbf906133b6565b60405180910390fd5b6000604060018989030181610bd957fe5b0614610c1a576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610c1190613396565b60405180910390fd5b8660026000838152602001908152602001600020600101541115610c73576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610c6a90613336565b60405180910390fd5b6000600260008a81526020019081526020016000206000015414610ccc576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401610cc390613376565b60405180910390fd5b604051806060016040528089815260200188815260200187815250600260008a8152602001908152602001600020600082015181600001556020820151816001015560408201518160020155905050600388908060018154018082558091505090600182039060005260206000200160009091929091909150555060008060008a815260200190815260200160002081610d669190612800565b506000600160008a815260200190815260200160002081610d879190612800565b506060610ddf610dda87878080601f016020809104026020016040519081016040528093929190818152602001838380828437600081840152601f19601f820116905080830192505050505050506122cb565b6122f9565b905060008090505b8151811015610f51576060610e0e838381518110610e0157fe5b60200260200101516122f9565b90506000808c81526020019081526020016000208054809190600101610e349190612800565b506040518060600160405280610e5d83600081518110610e5057fe5b60200260200101516123d6565b8152602001610e7f83600181518110610e7257fe5b60200260200101516123d6565b8152602001610ea183600281518110610e9457fe5b6020026020010151612447565b73ffffffffffffffffffffffffffffffffffffffff168152506000808d81526020019081526020016000208381548110610ed757fe5b9060005260206000209060030201600082015181600001556020820151816001015560408201518160020160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550905050508080600101915050610de7565b506060610fa9610fa486868080601f016020809104026020016040519081016040528093929190818152602001838380828437600081840152601f19601f820116905080830192505050505050506122cb565b6122f9565b905060008090505b815181101561111d576060610fd8838381518110610fcb57fe5b60200260200101516122f9565b9050600160008d81526020019081526020016000208054809190600101610fff9190612800565b5060405180606001604052806110288360008151811061101b57fe5b60200260200101516123d6565b815260200161104a8360018151811061103d57fe5b60200260200101516123d6565b815260200161106c8360028151811061105f57fe5b6020026020010151612447565b73ffffffffffffffffffffffffffffffffffffffff16815250600160008e815260200190815260200160002083815481106110a357fe5b9060005260206000209060030201600082015181600001556020820151816001015560408201518160020160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550905050508080600101915050610fb1565b5050505050505050505050565b60006060600080858152602001908152602001600020805480602002602001604051908101604052809291908181526020016000905b828210156111fc578382906000526020600020906003020160405180606001604052908160008201548152602001600182015481526020016002820160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152505081526020019060010190611160565b50505050905060008090505b8151811015611274578373ffffffffffffffffffffffffffffffffffffffff1682828151811061123457fe5b60200260200101516040015173ffffffffffffffffffffffffffffffffffffffff1614156112675760019250505061127b565b8080600101915050611208565b5060009150505b92915050565b604081565b60006002600160f81b84846040516020016112a3939291906131d6565b6040516020818303038152906040526040516112bf9190613213565b602060405180830381855afa1580156112dc573d6000803e3d6000fd5b5050506040513d601f19601f820116820180604052506112ff9190810190612a58565b905092915050565b60006060600080848152602001908152602001600020805480602002602001604051908101604052809291908181526020016000905b828210156113d9578382906000526020600020906003020160405180606001604052908160008201548152602001600182015481526020016002820160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815250508152602001906001019061133d565b505050509050600080905060008090505b825181101561142c5761141d83828151811061140257fe5b602002602001015160200151836122ac90919063ffffffff16565b915080806001019150506113ea565b508092505050919050565b73fffffffffffffffffffffffffffffffffffffffe81565b600080600080859050600060218087518161146657fe5b04029050600081111561147f5761147c876117ab565b91505b6000602190505b818111611509576000600182038801519050818801519550806000602081106114ab57fe5b1a60f81b9450600060f81b857effffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff191614156114f0576114e98685611286565b93506114fd565b6114fa8487611286565b93505b50602181019050611486565b508782149450505050509392505050565b60405161152690613254565b604051809103902081565b60008060009050600080905060008090505b84518167ffffffffffffffff16101561160c57606061156e868367ffffffffffffffff16604161246a565b9050600061158582896124f690919063ffffffff16565b905061158f612832565b6115998a83611619565b90506115a58a8361112a565b80156115dc57508473ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff16115b156115fe578194506115fb8160200151876122ac90919063ffffffff16565b95505b505050604181019050611543565b5081925050509392505050565b611621612832565b6060600080858152602001908152602001600020805480602002602001604051908101604052809291908181526020016000905b828210156116f1578382906000526020600020906003020160405180606001604052908160008201548152602001600182015481526020016002820160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152505081526020019060010190611655565b50505050905060008090505b8151811015611779578373ffffffffffffffffffffffffffffffffffffffff1682828151811061172957fe5b60200260200101516040015173ffffffffffffffffffffffffffffffffffffffff16141561176c5781818151811061175d57fe5b60200260200101519250611779565b80806001019150506116fd565b505092915050565b600061178c43611db1565b905090565b60006117a461179e611781565b8361112a565b9050919050565b60006002600060f81b836040516020016117c69291906131aa565b6040516020818303038152906040526040516117e29190613213565b602060405180830381855afa1580156117ff573d6000803e3d6000fd5b5050506040513d601f19601f820116820180604052506118229190810190612a58565b9050919050565b60008060008061184a600161183c611781565b6122ac90919063ffffffff16565b905060026000828152602001908152602001600020600001546002600083815260200190815260200160002060010154600260008481526020019081526020016000206002015493509350935050909192565b606080606060056040519080825280602002602001820160405280156118d25781602001602082028038833980820191505090505b50905073c26880a0af2ea0c7e8130e6ec47af756465452e8816000815181106118f757fe5b602002602001019073ffffffffffffffffffffffffffffffffffffffff16908173ffffffffffffffffffffffffffffffffffffffff168152505073be188d6641e8b680743a4815dfa0f6208038960f8160018151811061195357fe5b602002602001019073ffffffffffffffffffffffffffffffffffffffff16908173ffffffffffffffffffffffffffffffffffffffff168152505073c275dc8be39f50d12f66b6a63629c39da5bae5bd816002815181106119af57fe5b602002602001019073ffffffffffffffffffffffffffffffffffffffff16908173ffffffffffffffffffffffffffffffffffffffff168152505073f903ba9e006193c1527bfbe65fe2123704ea3f9981600381518110611a0b57fe5b602002602001019073ffffffffffffffffffffffffffffffffffffffff16908173ffffffffffffffffffffffffffffffffffffffff168152505073928ed6a3e94437bbd316ccad78479f1d163a6a8c81600481518110611a6757fe5b602002602001019073ffffffffffffffffffffffffffffffffffffffff16908173ffffffffffffffffffffffffffffffffffffffff168152505060606005604051908082528060200260200182016040528015611ad35781602001602082028038833980820191505090505b50905061271081600081518110611ae657fe5b60200260200101818152505061271081600181518110611b0257fe5b60200260200101818152505061271081600281518110611b1e57fe5b60200260200101818152505061271081600381518110611b3a57fe5b60200260200101818152505061271081600481518110611b5657fe5b60200260200101818152505081819350935050509091565b60ff81565b60016020528160005260406000208181548110611b8c57fe5b9060005260206000209060030201600091509150508060000154908060010154908060020160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff16905083565b6000611bea611be4611781565b83610939565b9050919050565b604051611bfd9061322a565b604051809103902081565b6000606060016000848152602001908152602001600020805480602002602001604051908101604052809291908181526020016000905b82821015611cdb578382906000526020600020906003020160405180606001604052908160008201548152602001600182015481526020016002820160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152505081526020019060010190611c3f565b505050509050600080905060008090505b8251811015611d2e57611d1f838281518110611d0457fe5b602002602001015160200151836122ac90919063ffffffff16565b91508080600101915050611cec565b508092505050919050565b604051611d459061323f565b604051809103902081565b600080600080611d5e611781565b905060026000828152602001908152602001600020600001546002600083815260200190815260200160002060010154600260008481526020019081526020016000206002015493509350935050909192565b60008060038054905090505b6000811115611e7157611dce612869565b6002600060036001850381548110611de257fe5b906000526020600020015481526020019081526020016000206040518060600160405290816000820154815260200160018201548152602001600282015481525050905083816020015111158015611e3f57506000816040015114155b8015611e4f575080604001518411155b15611e6257806000015192505050611eac565b50808060019003915050611dbd565b5060006003805490501115611ea757600360016003805490500381548110611e9557fe5b90600052602060002001549050611eac565b600090505b919050565b606080611ebd4361075d565b915091509091565b60038181548110611ed257fe5b906000526020600020016000915090505481565b600281565b60006020528160005260406000208181548110611f0457fe5b9060005260206000209060030201600091509150508060000154908060010154908060020160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff16905083565b600060404381611f5b57fe5b04905090565b60026020528060005260406000206000915090508060000154908060010154908060020154905083565b606080611f9661189d565b8092508193505050600080905060405180606001604052808281526020016000815260200160ff81525060026000838152602001908152602001600020600082015181600001556020820151816001015560408201518160020155905050600381908060018154018082558091505090600182039060005260206000200160009091929091909150555060008060008381526020019081526020016000208161203f9190612800565b50600060016000838152602001908152602001600020816120609190612800565b5060008090505b83518110156121825760008083815260200190815260200160002080548091906001016120949190612800565b5060405180606001604052808281526020018483815181106120b257fe5b602002602001015181526020018583815181106120cb57fe5b602002602001015173ffffffffffffffffffffffffffffffffffffffff16815250600080848152602001908152602001600020828154811061210957fe5b9060005260206000209060030201600082015181600001556020820151816001015560408201518160020160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055509050508080600101915050612067565b5060008090505b83518110156122a6576001600083815260200190815260200160002080548091906001016121b79190612800565b5060405180606001604052808281526020018483815181106121d557fe5b602002602001015181526020018583815181106121ee57fe5b602002602001015173ffffffffffffffffffffffffffffffffffffffff1681525060016000848152602001908152602001600020828154811061222d57fe5b9060005260206000209060030201600082015181600001556020820151816001015560408201518160020160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055509050508080600101915050612189565b50505050565b6000808284019050838110156122c157600080fd5b8091505092915050565b6122d361288a565b600060208301905060405180604001604052808451815260200182815250915050919050565b606061230482612600565b61230d57600080fd5b60006123188361264e565b905060608160405190808252806020026020018201604052801561235657816020015b6123436128a4565b81526020019060019003908161233b5790505b509050600061236885602001516126bf565b8560200151019050600080600090505b848110156123c95761238983612748565b91506040518060400160405280838152602001848152508482815181106123ac57fe5b602002602001018190525081830192508080600101915050612378565b5082945050505050919050565b60008082600001511180156123f057506021826000015111155b6123f957600080fd5b600061240883602001516126bf565b9050600081846000015103905060008083866020015101905080519150602083101561243b57826020036101000a820491505b81945050505050919050565b6000601582600001511461245a57600080fd5b612463826123d6565b9050919050565b60608183018451101561247c57600080fd5b6060821560008114612499576040519150602082016040526124ea565b6040519150601f8416801560200281840101858101878315602002848b0101015b818310156124d757805183526020830192506020810190506124ba565b50868552601f19601f8301166040525050505b50809150509392505050565b600080600080604185511461251157600093505050506125fa565b602085015192506040850151915060ff6041860151169050601b8160ff16101561253c57601b810190505b601b8160ff16141580156125545750601c8160ff1614155b1561256557600093505050506125fa565b60006001878386866040516000815260200160405260405161258a94939291906132f1565b6020604051602081039080840390855afa1580156125ac573d6000803e3d6000fd5b505050602060405103519050600073ffffffffffffffffffffffffffffffffffffffff168173ffffffffffffffffffffffffffffffffffffffff1614156125f257600080fd5b809450505050505b92915050565b600080826000015114156126175760009050612649565b60008083602001519050805160001a915060c060ff168260ff16101561264257600092505050612649565b6001925050505b919050565b6000808260000151141561266557600090506126ba565b6000809050600061267984602001516126bf565b84602001510190506000846000015185602001510190505b808210156126b3576126a282612748565b820191508280600101935050612691565b8293505050505b919050565b600080825160001a9050608060ff168110156126df576000915050612743565b60b860ff16811080612704575060c060ff168110158015612703575060f860ff1681105b5b15612713576001915050612743565b60c060ff168110156127335760018060b80360ff16820301915050612743565b60018060f80360ff168203019150505b919050565b6000806000835160001a9050608060ff1681101561276957600191506127f6565b60b860ff16811015612786576001608060ff1682030191506127f5565b60c060ff168110156127b65760b78103600185019450806020036101000a855104600182018101935050506127f4565b60f860ff168110156127d357600160c060ff1682030191506127f3565b60f78103600185019450806020036101000a855104600182018101935050505b5b5b5b8192505050919050565b81548183558181111561282d5760030281600302836000526020600020918201910161282c91906128be565b5b505050565b60405180606001604052806000815260200160008152602001600073ffffffffffffffffffffffffffffffffffffffff1681525090565b60405180606001604052806000815260200160008152602001600081525090565b604051806040016040528060008152602001600081525090565b604051806040016040528060008152602001600081525090565b61291191905b8082111561290d5760008082016000905560018201600090556002820160006101000a81549073ffffffffffffffffffffffffffffffffffffffff0219169055506003016128c4565b5090565b90565b60008135905061292381613693565b92915050565b600081359050612938816136aa565b92915050565b60008151905061294d816136aa565b92915050565b60008083601f84011261296557600080fd5b8235905067ffffffffffffffff81111561297e57600080fd5b60208301915083600182028301111561299657600080fd5b9250929050565b600082601f8301126129ae57600080fd5b81356129c16129bc826134e2565b6134b5565b915080825260208301602083018583830111156129dd57600080fd5b6129e883828461363d565b50505092915050565b600081359050612a00816136c1565b92915050565b600060208284031215612a1857600080fd5b6000612a2684828501612914565b91505092915050565b600060208284031215612a4157600080fd5b6000612a4f84828501612929565b91505092915050565b600060208284031215612a6a57600080fd5b6000612a788482850161293e565b91505092915050565b60008060408385031215612a9457600080fd5b6000612aa285828601612929565b9250506020612ab385828601612929565b9150509250929050565b600080600060608486031215612ad257600080fd5b6000612ae086828701612929565b9350506020612af186828701612929565b925050604084013567ffffffffffffffff811115612b0e57600080fd5b612b1a8682870161299d565b9150509250925092565b600060208284031215612b3657600080fd5b6000612b44848285016129f1565b91505092915050565b60008060408385031215612b6057600080fd5b6000612b6e858286016129f1565b9250506020612b7f85828601612914565b9150509250929050565b600080600060608486031215612b9e57600080fd5b6000612bac868287016129f1565b9350506020612bbd86828701612929565b925050604084013567ffffffffffffffff811115612bda57600080fd5b612be68682870161299d565b9150509250925092565b60008060408385031215612c0357600080fd5b6000612c11858286016129f1565b9250506020612c22858286016129f1565b9150509250929050565b600080600080600080600060a0888a031215612c4757600080fd5b6000612c558a828b016129f1565b9750506020612c668a828b016129f1565b9650506040612c778a828b016129f1565b955050606088013567ffffffffffffffff811115612c9457600080fd5b612ca08a828b01612953565b9450945050608088013567ffffffffffffffff811115612cbf57600080fd5b612ccb8a828b01612953565b925092505092959891949750929550565b6000612ce88383612d0c565b60208301905092915050565b6000612d00838361317d565b60208301905092915050565b612d15816135b2565b82525050565b612d24816135b2565b82525050565b6000612d358261352e565b612d3f8185613569565b9350612d4a8361350e565b8060005b83811015612d7b578151612d628882612cdc565b9750612d6d8361354f565b925050600181019050612d4e565b5085935050505092915050565b6000612d9382613539565b612d9d818561357a565b9350612da88361351e565b8060005b83811015612dd9578151612dc08882612cf4565b9750612dcb8361355c565b925050600181019050612dac565b5085935050505092915050565b612def816135c4565b82525050565b612e06612e01826135d0565b61367f565b82525050565b612e15816135fc565b82525050565b612e2c612e27826135fc565b613689565b82525050565b6000612e3d82613544565b612e47818561358b565b9350612e5781856020860161364c565b80840191505092915050565b6000612e706004836135a7565b91507f766f7465000000000000000000000000000000000000000000000000000000006000830152600482019050919050565b6000612eb0602d83613596565b91507f537461727420626c6f636b206d7573742062652067726561746572207468616e60008301527f2063757272656e74207370616e000000000000000000000000000000000000006020830152604082019050919050565b6000612f16600f83613596565b91507f496e76616c6964207370616e20696400000000000000000000000000000000006000830152602082019050919050565b6000612f56601383613596565b91507f5370616e20616c726561647920657869737473000000000000000000000000006000830152602082019050919050565b6000612f96604583613596565b91507f446966666572656e6365206265747765656e20737461727420616e6420656e6460008301527f20626c6f636b206d75737420626520696e206d756c7469706c6573206f66207360208301527f7072696e740000000000000000000000000000000000000000000000000000006040830152606082019050919050565b6000613022602a83613596565b91507f456e6420626c6f636b206d7573742062652067726561746572207468616e207360008301527f7461727420626c6f636b000000000000000000000000000000000000000000006020830152604082019050919050565b6000613088601283613596565b91507f4e6f742053797374656d204164646573732100000000000000000000000000006000830152602082019050919050565b60006130c86005836135a7565b91507f38303030310000000000000000000000000000000000000000000000000000006000830152600582019050919050565b6000613108600e836135a7565b91507f6865696d64616c6c2d38303030310000000000000000000000000000000000006000830152600e82019050919050565b606082016000820151613151600085018261317d565b506020820151613164602085018261317d565b5060408201516131776040850182612d0c565b50505050565b61318681613626565b82525050565b61319581613626565b82525050565b6131a481613630565b82525050565b60006131b68285612df5565b6001820191506131c68284612e1b565b6020820191508190509392505050565b60006131e28286612df5565b6001820191506131f28285612e1b565b6020820191506132028284612e1b565b602082019150819050949350505050565b600061321f8284612e32565b915081905092915050565b600061323582612e63565b9150819050919050565b600061324a826130bb565b9150819050919050565b600061325f826130fb565b9150819050919050565b600060208201905061327e6000830184612d1b565b92915050565b6000604082019050818103600083015261329e8185612d2a565b905081810360208301526132b28184612d88565b90509392505050565b60006020820190506132d06000830184612de6565b92915050565b60006020820190506132eb6000830184612e0c565b92915050565b60006080820190506133066000830187612e0c565b613313602083018661319b565b6133206040830185612e0c565b61332d6060830184612e0c565b95945050505050565b6000602082019050818103600083015261334f81612ea3565b9050919050565b6000602082019050818103600083015261336f81612f09565b9050919050565b6000602082019050818103600083015261338f81612f49565b9050919050565b600060208201905081810360008301526133af81612f89565b9050919050565b600060208201905081810360008301526133cf81613015565b9050919050565b600060208201905081810360008301526133ef8161307b565b9050919050565b600060608201905061340b600083018461313b565b92915050565b6000602082019050613426600083018461318c565b92915050565b6000606082019050613441600083018661318c565b61344e602083018561318c565b61345b6040830184612d1b565b949350505050565b6000606082019050613478600083018661318c565b613485602083018561318c565b613492604083018461318c565b949350505050565b60006020820190506134af600083018461319b565b92915050565b6000604051905081810181811067ffffffffffffffff821117156134d857600080fd5b8060405250919050565b600067ffffffffffffffff8211156134f957600080fd5b601f19601f8301169050602081019050919050565b6000819050602082019050919050565b6000819050602082019050919050565b600081519050919050565b600081519050919050565b600081519050919050565b6000602082019050919050565b6000602082019050919050565b600082825260208201905092915050565b600082825260208201905092915050565b600081905092915050565b600082825260208201905092915050565b600081905092915050565b60006135bd82613606565b9050919050565b60008115159050919050565b60007fff0000000000000000000000000000000000000000000000000000000000000082169050919050565b6000819050919050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000819050919050565b600060ff82169050919050565b82818337600083830152505050565b60005b8381101561366a57808201518184015260208101905061364f565b83811115613679576000848401525b50505050565b6000819050919050565b6000819050919050565b61369c816135b2565b81146136a757600080fd5b50565b6136b3816135fc565b81146136be57600080fd5b50565b6136ca81613626565b81146136d557600080fd5b5056fea365627a7a723158208f52ee07630ffe523cc6ad3e15f437f973dcfa36729cd697f9b0fc4a145a48f06c6578706572696d656e74616cf564736f6c634300050b0040" - }, - "0000000000000000000000000000000000001001": { - "balance": "0x0", - "code": "0x608060405234801561001057600080fd5b50600436106100415760003560e01c806319494a17146100465780633434735f146100e15780635407ca671461012b575b600080fd5b6100c76004803603604081101561005c57600080fd5b81019080803590602001909291908035906020019064010000000081111561008357600080fd5b82018360208201111561009557600080fd5b803590602001918460018302840111640100000000831117156100b757600080fd5b9091929391929390505050610149565b604051808215151515815260200191505060405180910390f35b6100e961047a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b610133610492565b6040518082815260200191505060405180910390f35b600073fffffffffffffffffffffffffffffffffffffffe73ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614610200576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260128152602001807f4e6f742053797374656d2041646465737321000000000000000000000000000081525060200191505060405180910390fd5b606061025761025285858080601f016020809104026020016040519081016040528093929190818152602001838380828437600081840152601f19601f82011690508083019250505050505050610498565b6104c6565b905060006102788260008151811061026b57fe5b60200260200101516105a3565b905080600160005401146102f4576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252601b8152602001807f537461746549647320617265206e6f742073657175656e7469616c000000000081525060200191505060405180910390fd5b600080815480929190600101919050555060006103248360018151811061031757fe5b6020026020010151610614565b905060606103458460028151811061033857fe5b6020026020010151610637565b9050610350826106c3565b1561046f576000624c4b409050606084836040516024018083815260200180602001828103825283818151815260200191508051906020019080838360005b838110156103aa57808201518184015260208101905061038f565b50505050905090810190601f1680156103d75780820380516001836020036101000a031916815260200191505b5093505050506040516020818303038152906040527f26c53bea000000000000000000000000000000000000000000000000000000007bffffffffffffffffffffffffffffffffffffffffffffffffffffffff19166020820180517bffffffffffffffffffffffffffffffffffffffffffffffffffffffff8381831617835250505050905060008082516020840160008887f1965050505b505050509392505050565b73fffffffffffffffffffffffffffffffffffffffe81565b60005481565b6104a0610943565b600060208301905060405180604001604052808451815260200182815250915050919050565b60606104d1826106dc565b6104da57600080fd5b60006104e58361072a565b905060608160405190808252806020026020018201604052801561052357816020015b61051061095d565b8152602001906001900390816105085790505b5090506000610535856020015161079b565b8560200151019050600080600090505b848110156105965761055683610824565b915060405180604001604052808381526020018481525084828151811061057957fe5b602002602001018190525081830192508080600101915050610545565b5082945050505050919050565b60008082600001511180156105bd57506021826000015111155b6105c657600080fd5b60006105d5836020015161079b565b9050600081846000015103905060008083866020015101905080519150602083101561060857826020036101000a820491505b81945050505050919050565b6000601582600001511461062757600080fd5b610630826105a3565b9050919050565b6060600082600001511161064a57600080fd5b6000610659836020015161079b565b905060008184600001510390506060816040519080825280601f01601f19166020018201604052801561069b5781602001600182028038833980820191505090505b50905060008160200190506106b78487602001510182856108dc565b81945050505050919050565b600080823b905060008163ffffffff1611915050919050565b600080826000015114156106f35760009050610725565b60008083602001519050805160001a915060c060ff168260ff16101561071e57600092505050610725565b6001925050505b919050565b600080826000015114156107415760009050610796565b60008090506000610755846020015161079b565b84602001510190506000846000015185602001510190505b8082101561078f5761077e82610824565b82019150828060010193505061076d565b8293505050505b919050565b600080825160001a9050608060ff168110156107bb57600091505061081f565b60b860ff168110806107e0575060c060ff1681101580156107df575060f860ff1681105b5b156107ef57600191505061081f565b60c060ff1681101561080f5760018060b80360ff1682030191505061081f565b60018060f80360ff168203019150505b919050565b6000806000835160001a9050608060ff1681101561084557600191506108d2565b60b860ff16811015610862576001608060ff1682030191506108d1565b60c060ff168110156108925760b78103600185019450806020036101000a855104600182018101935050506108d0565b60f860ff168110156108af57600160c060ff1682030191506108cf565b60f78103600185019450806020036101000a855104600182018101935050505b5b5b5b8192505050919050565b60008114156108ea5761093e565b5b602060ff16811061091a5782518252602060ff1683019250602060ff1682019150602060ff16810390506108eb565b6000600182602060ff16036101000a03905080198451168184511681811785525050505b505050565b604051806040016040528060008152602001600081525090565b60405180604001604052806000815260200160008152509056fea265627a7a7231582083fbdacb76f32b4112d0f7db9a596937925824798a0026ba0232322390b5263764736f6c634300050b0032" - }, - "0000000000000000000000000000000000001010": { - "balance": "0x204fcd4f31349d83b6e00000", - "code": "0x60806040526004361061019c5760003560e01c806377d32e94116100ec578063acd06cb31161008a578063e306f77911610064578063e306f77914610a7b578063e614d0d614610aa6578063f2fde38b14610ad1578063fc0c546a14610b225761019c565b8063acd06cb31461097a578063b789543c146109cd578063cc79f97b14610a505761019c565b80639025e64c116100c65780639025e64c146107c957806395d89b4114610859578063a9059cbb146108e9578063abceeba21461094f5761019c565b806377d32e94146106315780638da5cb5b146107435780638f32d59b1461079a5761019c565b806347e7ef24116101595780637019d41a116101335780637019d41a1461053357806370a082311461058a578063715018a6146105ef578063771282f6146106065761019c565b806347e7ef2414610410578063485cc9551461046b57806360f96a8f146104dc5761019c565b806306fdde03146101a15780631499c5921461023157806318160ddd1461028257806319d27d9c146102ad5780632e1a7d4d146103b1578063313ce567146103df575b600080fd5b3480156101ad57600080fd5b506101b6610b79565b6040518080602001828103825283818151815260200191508051906020019080838360005b838110156101f65780820151818401526020810190506101db565b50505050905090810190601f1680156102235780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b34801561023d57600080fd5b506102806004803603602081101561025457600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff169060200190929190505050610bb6565b005b34801561028e57600080fd5b50610297610c24565b6040518082815260200191505060405180910390f35b3480156102b957600080fd5b5061036f600480360360a08110156102d057600080fd5b81019080803590602001906401000000008111156102ed57600080fd5b8201836020820111156102ff57600080fd5b8035906020019184600183028401116401000000008311171561032157600080fd5b9091929391929390803590602001909291908035906020019092919080359060200190929190803573ffffffffffffffffffffffffffffffffffffffff169060200190929190505050610c3a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b6103dd600480360360208110156103c757600080fd5b8101908080359060200190929190505050610e06565b005b3480156103eb57600080fd5b506103f4610f58565b604051808260ff1660ff16815260200191505060405180910390f35b34801561041c57600080fd5b506104696004803603604081101561043357600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff16906020019092919080359060200190929190505050610f61565b005b34801561047757600080fd5b506104da6004803603604081101561048e57600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff169060200190929190803573ffffffffffffffffffffffffffffffffffffffff16906020019092919050505061111d565b005b3480156104e857600080fd5b506104f16111ec565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b34801561053f57600080fd5b50610548611212565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b34801561059657600080fd5b506105d9600480360360208110156105ad57600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff169060200190929190505050611238565b6040518082815260200191505060405180910390f35b3480156105fb57600080fd5b50610604611259565b005b34801561061257600080fd5b5061061b611329565b6040518082815260200191505060405180910390f35b34801561063d57600080fd5b506107016004803603604081101561065457600080fd5b81019080803590602001909291908035906020019064010000000081111561067b57600080fd5b82018360208201111561068d57600080fd5b803590602001918460018302840111640100000000831117156106af57600080fd5b91908080601f016020809104026020016040519081016040528093929190818152602001838380828437600081840152601f19601f82011690508083019250505050505050919291929050505061132f565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b34801561074f57600080fd5b506107586114b4565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b3480156107a657600080fd5b506107af6114dd565b604051808215151515815260200191505060405180910390f35b3480156107d557600080fd5b506107de611534565b6040518080602001828103825283818151815260200191508051906020019080838360005b8381101561081e578082015181840152602081019050610803565b50505050905090810190601f16801561084b5780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b34801561086557600080fd5b5061086e61156d565b6040518080602001828103825283818151815260200191508051906020019080838360005b838110156108ae578082015181840152602081019050610893565b50505050905090810190601f1680156108db5780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b610935600480360360408110156108ff57600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff169060200190929190803590602001909291905050506115aa565b604051808215151515815260200191505060405180910390f35b34801561095b57600080fd5b506109646115d0565b6040518082815260200191505060405180910390f35b34801561098657600080fd5b506109b36004803603602081101561099d57600080fd5b810190808035906020019092919050505061165d565b604051808215151515815260200191505060405180910390f35b3480156109d957600080fd5b50610a3a600480360360808110156109f057600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff16906020019092919080359060200190929190803590602001909291908035906020019092919050505061167d565b6040518082815260200191505060405180910390f35b348015610a5c57600080fd5b50610a6561169d565b6040518082815260200191505060405180910390f35b348015610a8757600080fd5b50610a906116a4565b6040518082815260200191505060405180910390f35b348015610ab257600080fd5b50610abb6116aa565b6040518082815260200191505060405180910390f35b348015610add57600080fd5b50610b2060048036036020811015610af457600080fd5b81019080803573ffffffffffffffffffffffffffffffffffffffff169060200190929190505050611737565b005b348015610b2e57600080fd5b50610b37611754565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b60606040518060400160405280600b81526020017f4d6174696320546f6b656e000000000000000000000000000000000000000000815250905090565b6040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260108152602001807f44697361626c656420666561747572650000000000000000000000000000000081525060200191505060405180910390fd5b6000601260ff16600a0a6402540be40002905090565b6000808511610c4857600080fd5b6000831480610c575750824311155b610cc9576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260148152602001807f5369676e6174757265206973206578706972656400000000000000000000000081525060200191505060405180910390fd5b6000610cd73387878761167d565b9050600015156005600083815260200190815260200160002060009054906101000a900460ff16151514610d73576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040180806020018281038252600f8152602001807f536967206465616374697661746564000000000000000000000000000000000081525060200191505060405180910390fd5b60016005600083815260200190815260200160002060006101000a81548160ff021916908315150217905550610ded8189898080601f016020809104026020016040519081016040528093929190818152602001838380828437600081840152601f19601f8201169050808301925050505050505061132f565b9150610dfa82848861177a565b50509695505050505050565b60003390506000610e1682611238565b9050610e2d83600654611b3790919063ffffffff16565b600681905550600083118015610e4257508234145b610eb4576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260138152602001807f496e73756666696369656e7420616d6f756e740000000000000000000000000081525060200191505060405180910390fd5b8173ffffffffffffffffffffffffffffffffffffffff16600260009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff167febff2602b3f468259e1e99f613fed6691f3a6526effe6ef3e768ba7ae7a36c4f8584610f3087611238565b60405180848152602001838152602001828152602001935050505060405180910390a3505050565b60006012905090565b610f696114dd565b610f7257600080fd5b600081118015610faf5750600073ffffffffffffffffffffffffffffffffffffffff168273ffffffffffffffffffffffffffffffffffffffff1614155b611004576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526023815260200180611e636023913960400191505060405180910390fd5b600061100f83611238565b905060008390508073ffffffffffffffffffffffffffffffffffffffff166108fc849081150290604051600060405180830381858888f1935050505015801561105c573d6000803e3d6000fd5b5061107283600654611b5790919063ffffffff16565b6006819055508373ffffffffffffffffffffffffffffffffffffffff16600260009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff167f4e2ca0515ed1aef1395f66b5303bb5d6f1bf9d61a353fa53f73f8ac9973fa9f685856110f489611238565b60405180848152602001838152602001828152602001935050505060405180910390a350505050565b600760009054906101000a900460ff1615611183576040517f08c379a0000000000000000000000000000000000000000000000000000000008152600401808060200182810382526023815260200180611e406023913960400191505060405180910390fd5b6001600760006101000a81548160ff02191690831515021790555080600260006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506111e882611b76565b5050565b600360009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b600460009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b60008173ffffffffffffffffffffffffffffffffffffffff16319050919050565b6112616114dd565b61126a57600080fd5b600073ffffffffffffffffffffffffffffffffffffffff166000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff167f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e060405160405180910390a360008060006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550565b60065481565b600080600080604185511461134a57600093505050506114ae565b602085015192506040850151915060ff6041860151169050601b8160ff16101561137557601b810190505b601b8160ff161415801561138d5750601c8160ff1614155b1561139e57600093505050506114ae565b60018682858560405160008152602001604052604051808581526020018460ff1660ff1681526020018381526020018281526020019450505050506020604051602081039080840390855afa1580156113fb573d6000803e3d6000fd5b505050602060405103519350600073ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff1614156114aa576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260128152602001807f4572726f7220696e2065637265636f766572000000000000000000000000000081525060200191505060405180910390fd5b5050505b92915050565b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff16905090565b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614905090565b6040518060400160405280600381526020017f013881000000000000000000000000000000000000000000000000000000000081525081565b60606040518060400160405280600581526020017f4d41544943000000000000000000000000000000000000000000000000000000815250905090565b60008134146115bc57600090506115ca565b6115c733848461177a565b90505b92915050565b6040518060800160405280605b8152602001611ed8605b91396040516020018082805190602001908083835b6020831061161f57805182526020820191506020810190506020830392506115fc565b6001836020036101000a0380198251168184511680821785525050505050509050019150506040516020818303038152906040528051906020012081565b60056020528060005260406000206000915054906101000a900460ff1681565b600061169361168e86868686611c6e565b611d44565b9050949350505050565b6201388181565b60015481565b604051806080016040528060528152602001611e86605291396040516020018082805190602001908083835b602083106116f957805182526020820191506020810190506020830392506116d6565b6001836020036101000a0380198251168184511680821785525050505050509050019150506040516020818303038152906040528051906020012081565b61173f6114dd565b61174857600080fd5b61175181611b76565b50565b600260009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000803073ffffffffffffffffffffffffffffffffffffffff166370a08231866040518263ffffffff1660e01b8152600401808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060206040518083038186803b1580156117fa57600080fd5b505afa15801561180e573d6000803e3d6000fd5b505050506040513d602081101561182457600080fd5b8101908080519060200190929190505050905060003073ffffffffffffffffffffffffffffffffffffffff166370a08231866040518263ffffffff1660e01b8152600401808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060206040518083038186803b1580156118b657600080fd5b505afa1580156118ca573d6000803e3d6000fd5b505050506040513d60208110156118e057600080fd5b810190808051906020019092919050505090506118fe868686611d8e565b8473ffffffffffffffffffffffffffffffffffffffff168673ffffffffffffffffffffffffffffffffffffffff16600260009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff167fe6497e3ee548a3372136af2fcb0696db31fc6cf20260707645068bd3fe97f3c48786863073ffffffffffffffffffffffffffffffffffffffff166370a082318e6040518263ffffffff1660e01b8152600401808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060206040518083038186803b158015611a0657600080fd5b505afa158015611a1a573d6000803e3d6000fd5b505050506040513d6020811015611a3057600080fd5b81019080805190602001909291905050503073ffffffffffffffffffffffffffffffffffffffff166370a082318e6040518263ffffffff1660e01b8152600401808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060206040518083038186803b158015611abe57600080fd5b505afa158015611ad2573d6000803e3d6000fd5b505050506040513d6020811015611ae857600080fd5b8101908080519060200190929190505050604051808681526020018581526020018481526020018381526020018281526020019550505050505060405180910390a46001925050509392505050565b600082821115611b4657600080fd5b600082840390508091505092915050565b600080828401905083811015611b6c57600080fd5b8091505092915050565b600073ffffffffffffffffffffffffffffffffffffffff168173ffffffffffffffffffffffffffffffffffffffff161415611bb057600080fd5b8073ffffffffffffffffffffffffffffffffffffffff166000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff167f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e060405160405180910390a3806000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555050565b6000806040518060800160405280605b8152602001611ed8605b91396040516020018082805190602001908083835b60208310611cc05780518252602082019150602081019050602083039250611c9d565b6001836020036101000a03801982511681845116808217855250505050505090500191505060405160208183030381529060405280519060200120905060405181815273ffffffffffffffffffffffffffffffffffffffff8716602082015285604082015284606082015283608082015260a0812092505081915050949350505050565b60008060015490506040517f190100000000000000000000000000000000000000000000000000000000000081528160028201528360228201526042812092505081915050919050565b8173ffffffffffffffffffffffffffffffffffffffff166108fc829081150290604051600060405180830381858888f19350505050158015611dd4573d6000803e3d6000fd5b508173ffffffffffffffffffffffffffffffffffffffff168373ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef836040518082815260200191505060405180910390a350505056fe54686520636f6e747261637420697320616c726561647920696e697469616c697a6564496e73756666696369656e7420616d6f756e74206f7220696e76616c69642075736572454950373132446f6d61696e28737472696e67206e616d652c737472696e672076657273696f6e2c75696e7432353620636861696e49642c6164647265737320766572696679696e67436f6e747261637429546f6b656e5472616e736665724f726465722861646472657373207370656e6465722c75696e7432353620746f6b656e49644f72416d6f756e742c6279746573333220646174612c75696e743235362065787069726174696f6e29a265627a7a723158208f81700133738d766ae3d68af591ad588b0125bd91449192179f460893f79f6b64736f6c634300050b0032" - }, - "C26880A0AF2EA0c7E8130e6EC47Af756465452E8": { - "balance": "0x3635c9adc5dea00000" - }, - "be188D6641E8b680743A4815dFA0f6208038960F": { - "balance": "0x3635c9adc5dea00000" - }, - "c275DC8bE39f50D12F66B6a63629C39dA5BAe5bd": { - "balance": "0x3635c9adc5dea00000" - }, - "F903ba9E006193c1527BfBe65fe2123704EA3F99": { - "balance": "0x3635c9adc5dea00000" - }, - "928Ed6A3e94437bbd316cCAD78479f1d163A6A8C": { - "balance": "0x3635c9adc5dea00000" - } - }, - "number": "0x0", - "gasUsed": "0x0", - "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000" -} diff --git a/builder/index.go b/builder/index.go new file mode 100644 index 0000000000..8a096dab3f --- /dev/null +++ b/builder/index.go @@ -0,0 +1,106 @@ +package builder + +import ( + "html/template" +) + +func parseIndexTemplate() (*template.Template, error) { + return template.New("index").Parse(` + + + + + + + + Boost Block Builder + + + + + + + + + + + + +
+
+

+ +

+ Boost Block Builder +

+

+ Pubkey {{ .Pubkey }} +

+

+

    +
  • Genesis fork version {{ .GenesisForkVersion }}
  • +
  • Bellatrix fork version {{ .BellatrixForkVersion }}
  • +
  • Genesis validators root {{ .GenesisValidatorsRoot }}
  • +
+

+

+

    +
  • Builder signing domain {{ .BuilderSigningDomain }}
  • +
  • Proposer signing domain {{ .ProposerSigningDomain }}
  • +
+

+ +

+

+ +

+ +
+ +

+

+ {{ .ValidatorsStats }} +

+

+ +
+ +

+

+ Best Header +

+
{{ .Header }}
+

+ +
+ +

+

+ Best Payload +

+
{{ .Blocks }}
+

+ +
+
+ + + +`) +} diff --git a/builder/local_relay.go b/builder/local_relay.go new file mode 100644 index 0000000000..9c2abef78c --- /dev/null +++ b/builder/local_relay.go @@ -0,0 +1,467 @@ +package builder + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "html/template" + "net/http" + "strconv" + "strings" + "sync" + "time" + + "github.com/attestantio/go-builder-client/api" + bellatrixapi "github.com/attestantio/go-builder-client/api/bellatrix" + capellaapi "github.com/attestantio/go-builder-client/api/capella" + apiv1 "github.com/attestantio/go-builder-client/api/v1" + "github.com/attestantio/go-builder-client/spec" + apiv1bellatrix "github.com/attestantio/go-eth2-client/api/v1/bellatrix" + consensusspec "github.com/attestantio/go-eth2-client/spec" + "github.com/attestantio/go-eth2-client/spec/bellatrix" + "github.com/attestantio/go-eth2-client/spec/phase0" + bellatrixutil "github.com/attestantio/go-eth2-client/util/bellatrix" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/log" + "github.com/flashbots/go-boost-utils/bls" + "github.com/flashbots/go-boost-utils/ssz" + "github.com/flashbots/go-boost-utils/utils" + "github.com/gorilla/mux" + "github.com/holiman/uint256" +) + +type ForkData struct { + GenesisForkVersion string + BellatrixForkVersion string + GenesisValidatorsRoot string +} + +type FullValidatorData struct { + ValidatorData + Timestamp uint64 +} + +type LocalRelay struct { + beaconClient IBeaconClient + + relaySecretKey *bls.SecretKey + relayPublicKey phase0.BLSPubKey + serializedRelayPubkey hexutil.Bytes + + builderSigningDomain phase0.Domain + proposerSigningDomain phase0.Domain + + validatorsLock sync.RWMutex + validators map[PubkeyHex]FullValidatorData + + enableBeaconChecks bool + + bestDataLock sync.Mutex + bestHeader *bellatrix.ExecutionPayloadHeader + bestPayload *bellatrix.ExecutionPayload + profit *uint256.Int + + indexTemplate *template.Template + fd ForkData +} + +func NewLocalRelay(sk *bls.SecretKey, beaconClient IBeaconClient, builderSigningDomain, proposerSigningDomain phase0.Domain, fd ForkData, enableBeaconChecks bool) (*LocalRelay, error) { + blsPk, err := bls.PublicKeyFromSecretKey(sk) + if err != nil { + return nil, err + } + pk, err := utils.BlsPublicKeyToPublicKey(blsPk) + if err != nil { + return nil, err + } + + indexTemplate, err := parseIndexTemplate() + if err != nil { + log.Error("could not parse index template", "err", err) + indexTemplate = nil + } + + return &LocalRelay{ + beaconClient: beaconClient, + + relaySecretKey: sk, + relayPublicKey: pk, + + builderSigningDomain: builderSigningDomain, + proposerSigningDomain: proposerSigningDomain, + serializedRelayPubkey: bls.PublicKeyToBytes(blsPk), + + validators: make(map[PubkeyHex]FullValidatorData), + + enableBeaconChecks: enableBeaconChecks, + + indexTemplate: indexTemplate, + fd: fd, + }, nil +} + +func (r *LocalRelay) Start() error { + r.beaconClient.Start() + return nil +} + +func (r *LocalRelay) Stop() { + r.beaconClient.Stop() +} + +func (r *LocalRelay) SubmitBlock(msg *bellatrixapi.SubmitBlockRequest, _ ValidatorData) error { + log.Info("submitting block to local relay", "block", msg.ExecutionPayload.BlockHash.String()) + return r.submitBlock(msg) +} + +func (r *LocalRelay) SubmitBlockCapella(msg *capellaapi.SubmitBlockRequest, _ ValidatorData) error { + log.Info("submitting block to local relay", "block", msg.ExecutionPayload.BlockHash.String()) + + return r.submitBlockCapella(msg) +} + +func (r *LocalRelay) Config() RelayConfig { + // local relay does not need config as it is submitting to its own internal endpoint + return RelayConfig{} +} + +// TODO: local relay support for capella +func (r *LocalRelay) submitBlockCapella(msg *capellaapi.SubmitBlockRequest) error { + return nil +} + +func (r *LocalRelay) submitBlock(msg *bellatrixapi.SubmitBlockRequest) error { + header, err := PayloadToPayloadHeader(msg.ExecutionPayload) + if err != nil { + log.Error("could not convert payload to header", "err", err) + return err + } + + r.bestDataLock.Lock() + r.bestHeader = header + r.bestPayload = msg.ExecutionPayload + r.profit = msg.Message.Value + r.bestDataLock.Unlock() + + return nil +} + +func (r *LocalRelay) handleRegisterValidator(w http.ResponseWriter, req *http.Request) { + payload := []apiv1.SignedValidatorRegistration{} + if err := json.NewDecoder(req.Body).Decode(&payload); err != nil { + log.Error("could not decode payload", "err", err) + respondError(w, http.StatusBadRequest, "invalid payload") + return + } + + for _, registerRequest := range payload { + if len(registerRequest.Message.Pubkey) != 48 { + respondError(w, http.StatusBadRequest, "invalid pubkey") + return + } + + if len(registerRequest.Signature) != 96 { + respondError(w, http.StatusBadRequest, "invalid signature") + return + } + + ok, err := ssz.VerifySignature(registerRequest.Message, r.builderSigningDomain, registerRequest.Message.Pubkey[:], registerRequest.Signature[:]) + if !ok || err != nil { + log.Error("error verifying signature", "err", err) + respondError(w, http.StatusBadRequest, "invalid signature") + return + } + + // Do not check timestamp before signature, as it would leak validator data + if registerRequest.Message.Timestamp.Unix() > time.Now().Add(10*time.Second).Unix() { + log.Error("invalid timestamp", "timestamp", registerRequest.Message.Timestamp) + respondError(w, http.StatusBadRequest, "invalid payload") + return + } + } + + for _, registerRequest := range payload { + pubkeyHex := PubkeyHex(registerRequest.Message.Pubkey.String()) + if !r.beaconClient.isValidator(pubkeyHex) { + respondError(w, http.StatusBadRequest, "not a validator") + return + } + } + + r.validatorsLock.Lock() + defer r.validatorsLock.Unlock() + + for _, registerRequest := range payload { + pubkeyHex := PubkeyHex(registerRequest.Message.Pubkey.String()) + if previousValidatorData, ok := r.validators[pubkeyHex]; ok { + if uint64(registerRequest.Message.Timestamp.Unix()) < previousValidatorData.Timestamp { + respondError(w, http.StatusBadRequest, "invalid timestamp") + return + } + + if uint64(registerRequest.Message.Timestamp.Unix()) == previousValidatorData.Timestamp && (registerRequest.Message.FeeRecipient != previousValidatorData.FeeRecipient || registerRequest.Message.GasLimit != previousValidatorData.GasLimit) { + respondError(w, http.StatusBadRequest, "invalid timestamp") + return + } + } + } + + for _, registerRequest := range payload { + pubkeyHex := PubkeyHex(strings.ToLower(registerRequest.Message.Pubkey.String())) + r.validators[pubkeyHex] = FullValidatorData{ + ValidatorData: ValidatorData{ + Pubkey: pubkeyHex, + FeeRecipient: registerRequest.Message.FeeRecipient, + GasLimit: registerRequest.Message.GasLimit, + }, + Timestamp: uint64(registerRequest.Message.Timestamp.Unix()), + } + + log.Info("registered validator", "pubkey", pubkeyHex, "data", r.validators[pubkeyHex]) + } + + w.WriteHeader(http.StatusOK) +} + +func (r *LocalRelay) GetValidatorForSlot(nextSlot uint64) (ValidatorData, error) { + pubkeyHex, err := r.beaconClient.getProposerForNextSlot(nextSlot) + if err != nil { + return ValidatorData{}, err + } + + r.validatorsLock.RLock() + if vd, ok := r.validators[pubkeyHex]; ok { + r.validatorsLock.RUnlock() + return vd.ValidatorData, nil + } + r.validatorsLock.RUnlock() + log.Info("no local entry for validator", "validator", pubkeyHex) + return ValidatorData{}, errors.New("missing validator") +} + +func (r *LocalRelay) handleGetHeader(w http.ResponseWriter, req *http.Request) { + vars := mux.Vars(req) + slot, err := strconv.Atoi(vars["slot"]) + if err != nil { + respondError(w, http.StatusBadRequest, "incorrect slot") + return + } + parentHashHex := vars["parent_hash"] + pubkeyHex := PubkeyHex(strings.ToLower(vars["pubkey"])) + + // Do not validate slot separately, it will create a race between slot update and proposer key + if nextSlotProposer, err := r.beaconClient.getProposerForNextSlot(uint64(slot)); err != nil || nextSlotProposer != pubkeyHex { + log.Error("getHeader requested for public key other than next slots proposer", "requested", pubkeyHex, "expected", nextSlotProposer) + w.WriteHeader(http.StatusNoContent) + return + } + + // Only check if slot is within a couple of the expected one, otherwise will force validators resync + vd, err := r.GetValidatorForSlot(uint64(slot)) + if err != nil { + respondError(w, http.StatusBadRequest, "unknown validator") + return + } + if vd.Pubkey != pubkeyHex { + respondError(w, http.StatusBadRequest, "unknown validator") + return + } + + r.bestDataLock.Lock() + bestHeader := r.bestHeader + profit := r.profit + r.bestDataLock.Unlock() + + if bestHeader == nil || bestHeader.ParentHash.String() != parentHashHex { + respondError(w, http.StatusBadRequest, "unknown payload") + return + } + + bid := bellatrixapi.BuilderBid{ + Header: bestHeader, + Value: profit, + Pubkey: r.relayPublicKey, + } + signature, err := ssz.SignMessage(&bid, r.builderSigningDomain, r.relaySecretKey) + if err != nil { + respondError(w, http.StatusInternalServerError, "internal server error") + return + } + + response := &spec.VersionedSignedBuilderBid{ + Version: consensusspec.DataVersionBellatrix, + Bellatrix: &bellatrixapi.SignedBuilderBid{Message: &bid, Signature: signature}, + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + if err := json.NewEncoder(w).Encode(response); err != nil { + respondError(w, http.StatusInternalServerError, "internal server error") + return + } +} + +func (r *LocalRelay) handleGetPayload(w http.ResponseWriter, req *http.Request) { + payload := new(apiv1bellatrix.SignedBlindedBeaconBlock) + if err := json.NewDecoder(req.Body).Decode(&payload); err != nil { + log.Error("failed to decode payload", "error", err) + respondError(w, http.StatusBadRequest, "invalid payload") + return + } + + if len(payload.Signature) != 96 { + respondError(w, http.StatusBadRequest, "invalid signature") + return + } + + nextSlotProposerPubkeyHex, err := r.beaconClient.getProposerForNextSlot(uint64(payload.Message.Slot)) + if err != nil { + if r.enableBeaconChecks { + respondError(w, http.StatusBadRequest, "unknown validator") + return + } + } + + nextSlotProposerPubkeyBytes, err := hexutil.Decode(string(nextSlotProposerPubkeyHex)) + if err != nil { + if r.enableBeaconChecks { + respondError(w, http.StatusBadRequest, "unknown validator") + return + } + } + + ok, err := ssz.VerifySignature(payload.Message, r.proposerSigningDomain, nextSlotProposerPubkeyBytes[:], payload.Signature[:]) + if !ok || err != nil { + if r.enableBeaconChecks { + respondError(w, http.StatusBadRequest, "invalid signature") + return + } + } + + r.bestDataLock.Lock() + bestHeader := r.bestHeader + bestPayload := r.bestPayload + r.bestDataLock.Unlock() + + log.Info("Received blinded block", "payload", payload, "bestHeader", bestHeader) + + if bestHeader == nil || bestPayload == nil { + respondError(w, http.StatusInternalServerError, "no payloads") + return + } + + if !ExecutionPayloadHeaderEqual(bestHeader, payload.Message.Body.ExecutionPayloadHeader) { + respondError(w, http.StatusBadRequest, "unknown payload") + return + } + + response := &api.VersionedExecutionPayload{ + Version: consensusspec.DataVersionBellatrix, + Bellatrix: bestPayload, + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + if err := json.NewEncoder(w).Encode(response); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + respondError(w, http.StatusInternalServerError, "internal server error") + return + } +} + +func (r *LocalRelay) handleIndex(w http.ResponseWriter, req *http.Request) { + if r.indexTemplate == nil { + http.Error(w, "not available", http.StatusInternalServerError) + } + + r.validatorsLock.RLock() + noValidators := len(r.validators) + r.validatorsLock.RUnlock() + validatorsStats := fmt.Sprint(noValidators) + " validators registered" + + r.bestDataLock.Lock() + header := r.bestHeader + payload := r.bestPayload + r.bestDataLock.Lock() + + headerData, err := json.MarshalIndent(header, "", " ") + if err != nil { + headerData = []byte{} + } + + payloadData, err := json.MarshalIndent(payload, "", " ") + if err != nil { + payloadData = []byte{} + } + + statusData := struct { + Pubkey string + ValidatorsStats string + GenesisForkVersion string + BellatrixForkVersion string + GenesisValidatorsRoot string + BuilderSigningDomain string + ProposerSigningDomain string + Header string + Blocks string + }{hexutil.Encode(r.serializedRelayPubkey), validatorsStats, r.fd.GenesisForkVersion, r.fd.BellatrixForkVersion, r.fd.GenesisValidatorsRoot, hexutil.Encode(r.builderSigningDomain[:]), hexutil.Encode(r.proposerSigningDomain[:]), string(headerData), string(payloadData)} + + if err := r.indexTemplate.Execute(w, statusData); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } +} + +type httpErrorResp struct { + Code int `json:"code"` + Message string `json:"message"` +} + +func respondError(w http.ResponseWriter, code int, message string) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(code) + if err := json.NewEncoder(w).Encode(httpErrorResp{code, message}); err != nil { + http.Error(w, message, code) + } +} + +func (r *LocalRelay) handleStatus(w http.ResponseWriter, req *http.Request) { + w.WriteHeader(http.StatusOK) +} + +func ExecutionPayloadHeaderEqual(l, r *bellatrix.ExecutionPayloadHeader) bool { + return l.ParentHash == r.ParentHash && l.FeeRecipient == r.FeeRecipient && l.StateRoot == r.StateRoot && l.ReceiptsRoot == r.ReceiptsRoot && l.LogsBloom == r.LogsBloom && l.PrevRandao == r.PrevRandao && l.BlockNumber == r.BlockNumber && l.GasLimit == r.GasLimit && l.GasUsed == r.GasUsed && l.Timestamp == r.Timestamp && l.BaseFeePerGas == r.BaseFeePerGas && bytes.Equal(l.ExtraData, r.ExtraData) && l.BlockHash == r.BlockHash && l.TransactionsRoot == r.TransactionsRoot +} + +// PayloadToPayloadHeader converts an ExecutionPayload to ExecutionPayloadHeader +func PayloadToPayloadHeader(p *bellatrix.ExecutionPayload) (*bellatrix.ExecutionPayloadHeader, error) { + if p == nil { + return nil, errors.New("nil payload") + } + + var txs []bellatrix.Transaction + txs = append(txs, p.Transactions...) + + transactions := bellatrixutil.ExecutionPayloadTransactions{Transactions: txs} + txroot, err := transactions.HashTreeRoot() + if err != nil { + return nil, err + } + + return &bellatrix.ExecutionPayloadHeader{ + ParentHash: p.ParentHash, + FeeRecipient: p.FeeRecipient, + StateRoot: p.StateRoot, + ReceiptsRoot: p.ReceiptsRoot, + LogsBloom: p.LogsBloom, + PrevRandao: p.PrevRandao, + BlockNumber: p.BlockNumber, + GasLimit: p.GasLimit, + GasUsed: p.GasUsed, + Timestamp: p.Timestamp, + ExtraData: p.ExtraData, + BaseFeePerGas: p.BaseFeePerGas, + BlockHash: p.BlockHash, + TransactionsRoot: txroot, + }, nil +} diff --git a/builder/local_relay_test.go b/builder/local_relay_test.go new file mode 100644 index 0000000000..4b6ae15630 --- /dev/null +++ b/builder/local_relay_test.go @@ -0,0 +1,279 @@ +package builder + +import ( + "bytes" + "encoding/json" + "fmt" + "math/big" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/attestantio/go-builder-client/api" + bellatrixapi "github.com/attestantio/go-builder-client/api/bellatrix" + apiv1 "github.com/attestantio/go-builder-client/api/v1" + "github.com/attestantio/go-builder-client/spec" + consensusapiv1bellatrix "github.com/attestantio/go-eth2-client/api/v1/bellatrix" + "github.com/attestantio/go-eth2-client/spec/altair" + "github.com/attestantio/go-eth2-client/spec/bellatrix" + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/ethereum/go-ethereum/beacon/engine" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/flashbotsextra" + "github.com/ethereum/go-ethereum/log" + "github.com/flashbots/go-boost-utils/bls" + "github.com/flashbots/go-boost-utils/ssz" + "github.com/holiman/uint256" + "github.com/stretchr/testify/require" + "golang.org/x/time/rate" +) + +func newTestBackend(t *testing.T, forkchoiceData *engine.ExecutableData, block *types.Block, blockValue *big.Int) (*Builder, *LocalRelay, *ValidatorPrivateData) { + validator := NewRandomValidator() + sk, _ := bls.GenerateRandomSecretKey() + bDomain := ssz.ComputeDomain(ssz.DomainTypeAppBuilder, [4]byte{0x02, 0x0, 0x0, 0x0}, phase0.Root{}) + genesisValidatorsRoot := phase0.Root(common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000000")) + cDomain := ssz.ComputeDomain(ssz.DomainTypeBeaconProposer, [4]byte{0x02, 0x0, 0x0, 0x0}, genesisValidatorsRoot) + beaconClient := &testBeaconClient{validator: validator} + localRelay, _ := NewLocalRelay(sk, beaconClient, bDomain, cDomain, ForkData{}, true) + ethService := &testEthereumService{synced: true, testExecutableData: forkchoiceData, testBlock: block, testBlockValue: blockValue} + builderArgs := BuilderArgs{ + sk: sk, + ds: flashbotsextra.NilDbService{}, + relay: localRelay, + builderSigningDomain: bDomain, + eth: ethService, + dryRun: false, + ignoreLatePayloadAttributes: false, + validator: nil, + beaconClient: beaconClient, + limiter: nil, + } + backend, _ := NewBuilder(builderArgs) + + backend.limiter = rate.NewLimiter(rate.Inf, 0) + + return backend, localRelay, validator +} + +func testRequest(t *testing.T, localRelay *LocalRelay, method, path string, payload any) *httptest.ResponseRecorder { + var req *http.Request + var err error + + if payload == nil { + req, err = http.NewRequest(method, path, nil) + } else { + payloadBytes, err2 := json.Marshal(payload) + fmt.Println(string(payloadBytes)) + require.NoError(t, err2) + req, err = http.NewRequest(method, path, bytes.NewReader(payloadBytes)) + } + + require.NoError(t, err) + rr := httptest.NewRecorder() + getRouter(localRelay).ServeHTTP(rr, req) + return rr +} + +func TestValidatorRegistration(t *testing.T) { + _, relay, _ := newTestBackend(t, nil, nil, nil) + + v := NewRandomValidator() + payload, err := prepareRegistrationMessage(t, relay.builderSigningDomain, v) + require.NoError(t, err) + + rr := testRequest(t, relay, "POST", "/eth/v1/builder/validators", payload) + require.Equal(t, http.StatusOK, rr.Code) + require.Contains(t, relay.validators, PubkeyHex(v.Pk.String())) + require.Equal(t, FullValidatorData{ValidatorData: ValidatorData{Pubkey: PubkeyHex(v.Pk.String()), FeeRecipient: payload[0].Message.FeeRecipient, GasLimit: payload[0].Message.GasLimit}, Timestamp: uint64(payload[0].Message.Timestamp.Unix())}, relay.validators[PubkeyHex(v.Pk.String())]) + + rr = testRequest(t, relay, "POST", "/eth/v1/builder/validators", payload) + require.Equal(t, http.StatusOK, rr.Code) + + payload[0].Message.Timestamp = payload[0].Message.Timestamp.Add(time.Second) + // Invalid signature + payload[0].Signature[len(payload[0].Signature)-1] = 0x00 + rr = testRequest(t, relay, "POST", "/eth/v1/builder/validators", payload) + require.Equal(t, http.StatusBadRequest, rr.Code) + require.Equal(t, `{"code":400,"message":"invalid signature"}`+"\n", rr.Body.String()) + + // TODO: cover all errors +} + +func prepareRegistrationMessage(t *testing.T, domain phase0.Domain, v *ValidatorPrivateData) ([]apiv1.SignedValidatorRegistration, error) { + var pubkey phase0.BLSPubKey + copy(pubkey[:], v.Pk) + require.Equal(t, []byte(v.Pk), pubkey[:]) + + msg := apiv1.ValidatorRegistration{ + FeeRecipient: bellatrix.ExecutionAddress{0x42}, + GasLimit: 15_000_000, + Timestamp: time.Now(), + Pubkey: pubkey, + } + + signature, err := v.Sign(&msg, domain) + require.NoError(t, err) + + return []apiv1.SignedValidatorRegistration{{ + Message: &msg, + Signature: signature, + }}, nil +} + +func registerValidator(t *testing.T, v *ValidatorPrivateData, relay *LocalRelay) { + payload, err := prepareRegistrationMessage(t, relay.builderSigningDomain, v) + require.NoError(t, err) + + log.Info("Registering", "payload", payload[0].Message) + rr := testRequest(t, relay, "POST", "/eth/v1/builder/validators", payload) + require.Equal(t, http.StatusOK, rr.Code) + require.Contains(t, relay.validators, PubkeyHex(v.Pk.String())) + require.Equal(t, FullValidatorData{ValidatorData: ValidatorData{Pubkey: PubkeyHex(v.Pk.String()), FeeRecipient: payload[0].Message.FeeRecipient, GasLimit: payload[0].Message.GasLimit}, Timestamp: uint64(payload[0].Message.Timestamp.Unix())}, relay.validators[PubkeyHex(v.Pk.String())]) +} + +func TestGetHeader(t *testing.T) { + forkchoiceData := &engine.ExecutableData{ + ParentHash: common.HexToHash("0xafafafa"), + FeeRecipient: common.Address{0x01}, + LogsBloom: types.Bloom{0x00, 0x05, 0x10}.Bytes(), + BlockHash: common.HexToHash("0x24e6998e4d2b4fd85f7f0d27ac4b87aaf0ec18e156e4b6e194ab5dadee0cd004"), + BaseFeePerGas: big.NewInt(12), + ExtraData: []byte{}, + } + + forkchoiceBlock, err := engine.ExecutableDataToBlock(*forkchoiceData) + require.NoError(t, err) + forkchoiceBlockProfit := big.NewInt(10) + + backend, relay, validator := newTestBackend(t, forkchoiceData, forkchoiceBlock, forkchoiceBlockProfit) + + path := fmt.Sprintf("/eth/v1/builder/header/%d/%s/%s", 0, forkchoiceData.ParentHash.Hex(), validator.Pk.String()) + rr := testRequest(t, relay, "GET", path, nil) + require.Equal(t, `{"code":400,"message":"unknown validator"}`+"\n", rr.Body.String()) + + registerValidator(t, validator, relay) + + rr = testRequest(t, relay, "GET", path, nil) + require.Equal(t, `{"code":400,"message":"unknown payload"}`+"\n", rr.Body.String()) + + path = fmt.Sprintf("/eth/v1/builder/header/%d/%s/%s", 0, forkchoiceData.ParentHash.Hex(), NewRandomValidator().Pk.String()) + rr = testRequest(t, relay, "GET", path, nil) + require.Equal(t, ``, rr.Body.String()) + require.Equal(t, 204, rr.Code) + + err = backend.OnPayloadAttribute(&types.BuilderPayloadAttributes{}) + require.NoError(t, err) + time.Sleep(2 * time.Second) + + path = fmt.Sprintf("/eth/v1/builder/header/%d/%s/%s", 0, forkchoiceData.ParentHash.Hex(), validator.Pk.String()) + rr = testRequest(t, relay, "GET", path, nil) + require.Equal(t, http.StatusOK, rr.Code) + + bid := new(spec.VersionedSignedBuilderBid) + err = json.Unmarshal(rr.Body.Bytes(), bid) + require.NoError(t, err) + + executionPayload, err := executableDataToExecutionPayload(forkchoiceData) + require.NoError(t, err) + expectedHeader, err := PayloadToPayloadHeader(executionPayload) + require.NoError(t, err) + expectedValue, ok := uint256.FromBig(forkchoiceBlockProfit) + require.False(t, ok) + require.EqualValues(t, &bellatrixapi.BuilderBid{ + Header: expectedHeader, + Value: expectedValue, + Pubkey: backend.builderPublicKey, + }, bid.Bellatrix.Message) + + require.Equal(t, forkchoiceData.ParentHash.Bytes(), bid.Bellatrix.Message.Header.ParentHash[:], "didn't build on expected parent") + ok, err = ssz.VerifySignature(bid.Bellatrix.Message, backend.builderSigningDomain, backend.builderPublicKey[:], bid.Bellatrix.Signature[:]) + + require.NoError(t, err) + require.True(t, ok) +} + +func TestGetPayload(t *testing.T) { + forkchoiceData := &engine.ExecutableData{ + ParentHash: common.HexToHash("0xafafafa"), + FeeRecipient: common.Address{0x01}, + LogsBloom: types.Bloom{}.Bytes(), + BlockHash: common.HexToHash("0xc4a012b67027b3ab6c00acd31aeee24aa1515d6a5d7e81b0ee2e69517fdc387f"), + BaseFeePerGas: big.NewInt(12), + ExtraData: []byte{}, + } + + forkchoiceBlock, err := engine.ExecutableDataToBlock(*forkchoiceData) + require.NoError(t, err) + forkchoiceBlockProfit := big.NewInt(10) + + backend, relay, validator := newTestBackend(t, forkchoiceData, forkchoiceBlock, forkchoiceBlockProfit) + + registerValidator(t, validator, relay) + err = backend.OnPayloadAttribute(&types.BuilderPayloadAttributes{}) + require.NoError(t, err) + time.Sleep(2 * time.Second) + + path := fmt.Sprintf("/eth/v1/builder/header/%d/%s/%s", 0, forkchoiceData.ParentHash.Hex(), validator.Pk.String()) + rr := testRequest(t, relay, "GET", path, nil) + require.Equal(t, http.StatusOK, rr.Code) + + bid := new(spec.VersionedSignedBuilderBid) + err = json.Unmarshal(rr.Body.Bytes(), bid) + require.NoError(t, err) + + blockHash := [32]byte{0x06} + syncCommitteeBits := [64]byte{0x07} + + // Create request payload + msg := &consensusapiv1bellatrix.BlindedBeaconBlock{ + Slot: 1, + ProposerIndex: 2, + ParentRoot: phase0.Root{0x03}, + StateRoot: phase0.Root{0x04}, + Body: &consensusapiv1bellatrix.BlindedBeaconBlockBody{ + ETH1Data: &phase0.ETH1Data{ + DepositRoot: phase0.Root{0x05}, + DepositCount: 5, + BlockHash: blockHash[:], + }, + ProposerSlashings: []*phase0.ProposerSlashing{}, + AttesterSlashings: []*phase0.AttesterSlashing{}, + Attestations: []*phase0.Attestation{}, + Deposits: []*phase0.Deposit{}, + VoluntaryExits: []*phase0.SignedVoluntaryExit{}, + SyncAggregate: &altair.SyncAggregate{ + SyncCommitteeBits: syncCommitteeBits[:], + SyncCommitteeSignature: phase0.BLSSignature{0x08}, + }, + ExecutionPayloadHeader: bid.Bellatrix.Message.Header, + }, + } + + // TODO: test wrong signing domain + signature, err := validator.Sign(msg, relay.proposerSigningDomain) + require.NoError(t, err) + + // Call getPayload with invalid signature + rr = testRequest(t, relay, "POST", "/eth/v1/builder/blinded_blocks", &consensusapiv1bellatrix.SignedBlindedBeaconBlock{ + Message: msg, + Signature: phase0.BLSSignature{0x09}, + }) + require.Equal(t, http.StatusBadRequest, rr.Code) + require.Equal(t, `{"code":400,"message":"invalid signature"}`+"\n", rr.Body.String()) + + // Call getPayload with correct signature + rr = testRequest(t, relay, "POST", "/eth/v1/builder/blinded_blocks", &consensusapiv1bellatrix.SignedBlindedBeaconBlock{ + Message: msg, + Signature: signature, + }) + + // Verify getPayload response + require.Equal(t, http.StatusOK, rr.Code) + getPayloadResponse := new(api.VersionedExecutionPayload) + err = json.Unmarshal(rr.Body.Bytes(), getPayloadResponse) + require.NoError(t, err) + require.Equal(t, bid.Bellatrix.Message.Header.BlockHash, getPayloadResponse.Bellatrix.BlockHash) +} diff --git a/builder/relay.go b/builder/relay.go new file mode 100644 index 0000000000..a28fe1e71c --- /dev/null +++ b/builder/relay.go @@ -0,0 +1,229 @@ +package builder + +import ( + "context" + "errors" + "fmt" + "net/http" + "strings" + "sync" + "time" + + "github.com/attestantio/go-builder-client/api/bellatrix" + "github.com/attestantio/go-builder-client/api/capella" + "github.com/ethereum/go-ethereum/log" + "github.com/flashbots/go-boost-utils/utils" +) + +var ErrValidatorNotFound = errors.New("validator not found") + +type RemoteRelay struct { + client http.Client + config RelayConfig + + localRelay *LocalRelay + + cancellationsEnabled bool + + validatorsLock sync.RWMutex + validatorSyncOngoing bool + lastRequestedSlot uint64 + validatorSlotMap map[uint64]ValidatorData +} + +func NewRemoteRelay(config RelayConfig, localRelay *LocalRelay, cancellationsEnabled bool) *RemoteRelay { + r := &RemoteRelay{ + client: http.Client{Timeout: time.Second}, + localRelay: localRelay, + cancellationsEnabled: cancellationsEnabled, + validatorSyncOngoing: false, + lastRequestedSlot: 0, + validatorSlotMap: make(map[uint64]ValidatorData), + config: config, + } + + err := r.updateValidatorsMap(0, 3) + if err != nil { + log.Error("could not connect to remote relay, continuing anyway", "err", err) + } + return r +} + +type GetValidatorRelayResponse []struct { + Slot uint64 `json:"slot,string"` + Entry struct { + Message struct { + FeeRecipient string `json:"fee_recipient"` + GasLimit uint64 `json:"gas_limit,string"` + Timestamp uint64 `json:"timestamp,string"` + Pubkey string `json:"pubkey"` + } `json:"message"` + Signature string `json:"signature"` + } `json:"entry"` +} + +func (r *RemoteRelay) updateValidatorsMap(currentSlot uint64, retries int) error { + r.validatorsLock.Lock() + if r.validatorSyncOngoing { + r.validatorsLock.Unlock() + return errors.New("sync is ongoing") + } + r.validatorSyncOngoing = true + r.validatorsLock.Unlock() + + log.Info("requesting ", "currentSlot", currentSlot) + newMap, err := r.getSlotValidatorMapFromRelay() + for err != nil && retries > 0 { + log.Error("could not get validators map from relay, retrying", "err", err) + time.Sleep(time.Second) + newMap, err = r.getSlotValidatorMapFromRelay() + retries -= 1 + } + r.validatorsLock.Lock() + r.validatorSyncOngoing = false + if err != nil { + r.validatorsLock.Unlock() + log.Error("could not get validators map from relay", "err", err) + return err + } + + r.validatorSlotMap = newMap + r.lastRequestedSlot = currentSlot + r.validatorsLock.Unlock() + + log.Info("Updated validators", "count", len(newMap), "slot", currentSlot) + return nil +} + +func (r *RemoteRelay) GetValidatorForSlot(nextSlot uint64) (ValidatorData, error) { + // next slot is expected to be the actual chain's next slot, not something requested by the user! + // if not sanitized it will force resync of validator data and possibly is a DoS vector + + r.validatorsLock.RLock() + if r.lastRequestedSlot == 0 || nextSlot/32 > r.lastRequestedSlot/32 { + // Every epoch request validators map + go func() { + err := r.updateValidatorsMap(nextSlot, 1) + if err != nil { + log.Error("could not update validators map", "err", err) + } + }() + } + + vd, found := r.validatorSlotMap[nextSlot] + r.validatorsLock.RUnlock() + + if r.localRelay != nil { + localValidator, err := r.localRelay.GetValidatorForSlot(nextSlot) + if err == nil { + log.Info("Validator registration overwritten by local data", "slot", nextSlot, "validator", localValidator) + return localValidator, nil + } + } + + if found { + return vd, nil + } + + return ValidatorData{}, ErrValidatorNotFound +} + +func (r *RemoteRelay) Start() error { + return nil +} + +func (r *RemoteRelay) Stop() {} + +func (r *RemoteRelay) SubmitBlock(msg *bellatrix.SubmitBlockRequest, _ ValidatorData) error { + log.Info("submitting block to remote relay", "endpoint", r.config.Endpoint) + endpoint := r.config.Endpoint + "/relay/v1/builder/blocks" + if r.cancellationsEnabled { + endpoint = endpoint + "?cancellations=true" + } + code, err := SendHTTPRequest(context.TODO(), *http.DefaultClient, http.MethodPost, endpoint, msg, nil) + if err != nil { + return fmt.Errorf("error sending http request to relay %s. err: %w", r.config.Endpoint, err) + } + if code > 299 { + return fmt.Errorf("non-ok response code %d from relay %s", code, r.config.Endpoint) + } + + if r.localRelay != nil { + r.localRelay.submitBlock(msg) + } + + return nil +} + +func (r *RemoteRelay) SubmitBlockCapella(msg *capella.SubmitBlockRequest, _ ValidatorData) error { + log.Info("submitting block to remote relay", "endpoint", r.config.Endpoint) + + endpoint := r.config.Endpoint + "/relay/v1/builder/blocks" + if r.cancellationsEnabled { + endpoint = endpoint + "?cancellations=true" + } + + if r.config.SszEnabled { + bodyBytes, err := msg.MarshalSSZ() + if err != nil { + return fmt.Errorf("error marshaling ssz: %w", err) + } + log.Debug("submitting block to remote relay", "endpoint", r.config.Endpoint) + code, err := SendSSZRequest(context.TODO(), *http.DefaultClient, http.MethodPost, endpoint, bodyBytes, r.config.GzipEnabled) + if err != nil { + return fmt.Errorf("error sending http request to relay %s. err: %w", r.config.Endpoint, err) + } + if code > 299 { + return fmt.Errorf("non-ok response code %d from relay %s", code, r.config.Endpoint) + } + } else { + code, err := SendHTTPRequest(context.TODO(), *http.DefaultClient, http.MethodPost, endpoint, msg, nil) + if err != nil { + return fmt.Errorf("error sending http request to relay %s. err: %w", r.config.Endpoint, err) + } + if code > 299 { + return fmt.Errorf("non-ok response code %d from relay %s", code, r.config.Endpoint) + } + } + + if r.localRelay != nil { + r.localRelay.submitBlockCapella(msg) + } + + return nil +} + +func (r *RemoteRelay) getSlotValidatorMapFromRelay() (map[uint64]ValidatorData, error) { + var dst GetValidatorRelayResponse + code, err := SendHTTPRequest(context.TODO(), *http.DefaultClient, http.MethodGet, r.config.Endpoint+"/relay/v1/builder/validators", nil, &dst) + if err != nil { + return nil, err + } + + if code > 299 { + return nil, fmt.Errorf("non-ok response code %d from relay", code) + } + + res := make(map[uint64]ValidatorData) + for _, data := range dst { + feeRecipient, err := utils.HexToAddress(data.Entry.Message.FeeRecipient) + if err != nil { + log.Error("Ill-formatted fee_recipient from relay", "data", data) + continue + } + + pubkeyHex := PubkeyHex(strings.ToLower(data.Entry.Message.Pubkey)) + + res[data.Slot] = ValidatorData{ + Pubkey: pubkeyHex, + FeeRecipient: feeRecipient, + GasLimit: data.Entry.Message.GasLimit, + } + } + + return res, nil +} + +func (r *RemoteRelay) Config() RelayConfig { + return r.config +} diff --git a/builder/relay_aggregator.go b/builder/relay_aggregator.go new file mode 100644 index 0000000000..12f6f62c13 --- /dev/null +++ b/builder/relay_aggregator.go @@ -0,0 +1,169 @@ +package builder + +import ( + "errors" + "fmt" + "sync" + + "github.com/attestantio/go-builder-client/api/bellatrix" + "github.com/attestantio/go-builder-client/api/capella" + "github.com/ethereum/go-ethereum/log" +) + +type RemoteRelayAggregator struct { + relays []IRelay // in order of precedence, primary first + + registrationsCacheLock sync.RWMutex + registrationsCacheSlot uint64 + registrationsCache map[ValidatorData][]IRelay +} + +func NewRemoteRelayAggregator(primary IRelay, secondary []IRelay) *RemoteRelayAggregator { + relays := []IRelay{primary} + return &RemoteRelayAggregator{ + relays: append(relays, secondary...), + } +} + +func (r *RemoteRelayAggregator) Start() error { + for _, relay := range r.relays { + err := relay.Start() + if err != nil { + return err + } + } + return nil +} + +func (r *RemoteRelayAggregator) Stop() { + for _, relay := range r.relays { + relay.Stop() + } +} + +func (r *RemoteRelayAggregator) SubmitBlock(msg *bellatrix.SubmitBlockRequest, registration ValidatorData) error { + r.registrationsCacheLock.RLock() + defer r.registrationsCacheLock.RUnlock() + + relays, found := r.registrationsCache[registration] + if !found { + return fmt.Errorf("no relays for registration %s", registration.Pubkey) + } + for _, relay := range relays { + go func(relay IRelay) { + err := relay.SubmitBlock(msg, registration) + if err != nil { + log.Error("could not submit block", "err", err) + } + }(relay) + } + + return nil +} + +func (r *RemoteRelayAggregator) SubmitBlockCapella(msg *capella.SubmitBlockRequest, registration ValidatorData) error { + r.registrationsCacheLock.RLock() + defer r.registrationsCacheLock.RUnlock() + + relays, found := r.registrationsCache[registration] + if !found { + return fmt.Errorf("no relays for registration %s", registration.Pubkey) + } + for _, relay := range relays { + go func(relay IRelay) { + err := relay.SubmitBlockCapella(msg, registration) + if err != nil { + log.Error("could not submit block", "err", err) + } + }(relay) + } + + return nil +} + +type RelayValidatorRegistration struct { + vd ValidatorData + relayI int // index into relays array to preserve relative order +} + +func (r *RemoteRelayAggregator) GetValidatorForSlot(nextSlot uint64) (ValidatorData, error) { + registrationsCh := make(chan *RelayValidatorRegistration, len(r.relays)) + for i, relay := range r.relays { + go func(relay IRelay, relayI int) { + vd, err := relay.GetValidatorForSlot(nextSlot) + if err == nil { + registrationsCh <- &RelayValidatorRegistration{vd: vd, relayI: relayI} + } else if errors.Is(err, ErrValidatorNotFound) { + registrationsCh <- nil + } else { + log.Error("could not get validator registration", "err", err) + registrationsCh <- nil + } + }(relay, i) + } + + topRegistrationCh := make(chan ValidatorData, 1) + go r.updateRelayRegistrations(nextSlot, registrationsCh, topRegistrationCh) + + if vd, ok := <-topRegistrationCh; ok { + return vd, nil + } + return ValidatorData{}, ErrValidatorNotFound +} + +func (r *RemoteRelayAggregator) updateRelayRegistrations(nextSlot uint64, registrationsCh chan *RelayValidatorRegistration, topRegistrationCh chan ValidatorData) { + defer close(topRegistrationCh) + + r.registrationsCacheLock.Lock() + defer r.registrationsCacheLock.Unlock() + + if nextSlot < r.registrationsCacheSlot { + // slot is outdated + return + } + + registrations := make([]*RelayValidatorRegistration, 0, len(r.relays)) + bestRelayIndex := len(r.relays) // relay index of the topmost relay that gave us the registration + bestRelayRegistrationIndex := -1 // index into the registrations for the registration returned by topmost relay + for i := 0; i < len(r.relays); i++ { + relayRegistration := <-registrationsCh + if relayRegistration != nil { + registrations = append(registrations, relayRegistration) + // happy path for primary + if relayRegistration.relayI == 0 { + topRegistrationCh <- relayRegistration.vd + } + if relayRegistration.relayI < bestRelayIndex { + bestRelayIndex = relayRegistration.relayI + bestRelayRegistrationIndex = len(registrations) - 1 + } + } + } + + if len(registrations) == 0 { + return + } + + if bestRelayIndex != 0 { + // if bestRelayIndex == 0 it was already sent + topRegistrationCh <- registrations[bestRelayRegistrationIndex].vd + } + + if nextSlot == r.registrationsCacheSlot { + return + } + + if nextSlot > r.registrationsCacheSlot { + // clear the cache + r.registrationsCache = make(map[ValidatorData][]IRelay) + r.registrationsCacheSlot = nextSlot + } + + for _, relayRegistration := range registrations { + r.registrationsCache[relayRegistration.vd] = append(r.registrationsCache[relayRegistration.vd], r.relays[relayRegistration.relayI]) + } +} + +func (r *RemoteRelayAggregator) Config() RelayConfig { + return RelayConfig{} +} diff --git a/builder/relay_aggregator_test.go b/builder/relay_aggregator_test.go new file mode 100644 index 0000000000..d46533e49a --- /dev/null +++ b/builder/relay_aggregator_test.go @@ -0,0 +1,238 @@ +package builder + +import ( + "errors" + "testing" + "time" + + "github.com/attestantio/go-builder-client/api/bellatrix" + "github.com/attestantio/go-builder-client/api/capella" + "github.com/stretchr/testify/require" +) + +/* + validator ValidatorData + requestedSlot uint64 + submittedMsg *bellatrix.SubmitBlockRequest +*/ + +type testRelay struct { + sbError error + gvsVd ValidatorData + gvsErr error + + requestedSlot uint64 + submittedMsg *bellatrix.SubmitBlockRequest + submittedMsgCh chan *bellatrix.SubmitBlockRequest + submittedMsgCapella *capella.SubmitBlockRequest + submittedMsgChCapella chan *capella.SubmitBlockRequest +} + +type testRelayAggBackend struct { + relays []*testRelay + ragg *RemoteRelayAggregator +} + +func newTestRelayAggBackend(numRelay int) *testRelayAggBackend { + testRelays := make([]*testRelay, numRelay) + secondaryRelays := make([]IRelay, numRelay-1) + for i := 0; i < numRelay; i++ { + testRelays[i] = &testRelay{} + if i > 0 { + secondaryRelays[i-1] = testRelays[i] + } + } + ragg := NewRemoteRelayAggregator(testRelays[0], secondaryRelays) + return &testRelayAggBackend{testRelays, ragg} +} + +func (r *testRelay) SubmitBlock(msg *bellatrix.SubmitBlockRequest, registration ValidatorData) error { + if r.submittedMsgCh != nil { + select { + case r.submittedMsgCh <- msg: + default: + } + } + r.submittedMsg = msg + return r.sbError +} + +func (r *testRelay) SubmitBlockCapella(msg *capella.SubmitBlockRequest, registration ValidatorData) error { + if r.submittedMsgCh != nil { + select { + case r.submittedMsgChCapella <- msg: + default: + } + } + r.submittedMsgCapella = msg + return r.sbError +} + +func (r *testRelay) GetValidatorForSlot(nextSlot uint64) (ValidatorData, error) { + r.requestedSlot = nextSlot + return r.gvsVd, r.gvsErr +} + +func (r *testRelay) Start() error { + return nil +} + +func (r *testRelay) Stop() {} + +func (r *testRelay) Config() RelayConfig { + return RelayConfig{} +} + +func TestRemoteRelayAggregator(t *testing.T) { + t.Run("should return error if no relays return validator data", func(t *testing.T) { + backend := newTestRelayAggBackend(3) + // make all error out + for _, r := range backend.relays { + r.gvsErr = errors.New("error!") + } + + // Check getting validator slot - should error out if no relays return + _, err := backend.ragg.GetValidatorForSlot(10) + require.Error(t, ErrValidatorNotFound, err) + }) + + t.Run("should return validator if one relay returns validator data", func(t *testing.T) { + backend := newTestRelayAggBackend(3) + + // If primary returns should not error out + backend.relays[1].gvsErr = errors.New("error!") + backend.relays[2].gvsErr = errors.New("error!") + _, err := backend.ragg.GetValidatorForSlot(10) + require.NoError(t, err) + + // If any returns should not error out + backend.relays[0].gvsErr = errors.New("error!") + backend.relays[2].gvsErr = nil + _, err = backend.ragg.GetValidatorForSlot(10) + require.NoError(t, err) + }) + + t.Run("should return the more important (lower index) relay validator data", func(t *testing.T) { + backend := newTestRelayAggBackend(3) + + // Should return the more important relay if primary fails + backend.relays[0].gvsErr = errors.New("error!") + backend.relays[1].gvsVd.GasLimit = 20 + backend.relays[2].gvsVd.GasLimit = 30 + vd, err := backend.ragg.GetValidatorForSlot(10) + require.NoError(t, err) + require.Equal(t, uint64(20), vd.GasLimit) + + // Should return the primary if it returns + backend.relays[0].gvsErr = nil + backend.relays[0].gvsVd.GasLimit = 10 + vd, err = backend.ragg.GetValidatorForSlot(11) + require.NoError(t, err) + require.Equal(t, uint64(10), vd.GasLimit) + }) + + t.Run("should error submitting to unseen validator data", func(t *testing.T) { + backend := newTestRelayAggBackend(3) + + backend.relays[0].gvsVd.GasLimit = 10 + + vd, err := backend.ragg.GetValidatorForSlot(10) + require.NoError(t, err) + require.Equal(t, uint64(10), vd.GasLimit) + + // let the validator registrations finish + // TODO: notify from the test relays + time.Sleep(10 * time.Millisecond) + + // if submitting for unseen VD should error out + msg := &bellatrix.SubmitBlockRequest{} + err = backend.ragg.SubmitBlock(msg, ValidatorData{GasLimit: 40}) + require.Error(t, err) + }) + + t.Run("should submit to relay with matching validator data", func(t *testing.T) { + backend := newTestRelayAggBackend(3) + + backend.relays[0].gvsVd.GasLimit = 10 + backend.relays[1].gvsVd.GasLimit = 20 + backend.relays[2].gvsVd.GasLimit = 30 + + vd, err := backend.ragg.GetValidatorForSlot(11) + require.NoError(t, err) + require.Equal(t, uint64(10), vd.GasLimit) + + // let the validator registrations finish + // TODO: notify from the test relays + time.Sleep(10 * time.Millisecond) + + // if submitting for unseen VD should error out + msg := &bellatrix.SubmitBlockRequest{} + err = backend.ragg.SubmitBlock(msg, ValidatorData{GasLimit: 40}) + require.Error(t, err) + + // should submit to the single pirmary if its the only one matching + backend.relays[0].submittedMsgCh = make(chan *bellatrix.SubmitBlockRequest, 1) + err = backend.ragg.SubmitBlock(msg, ValidatorData{GasLimit: 10}) + require.NoError(t, err) + select { + case rsMsg := <-backend.relays[0].submittedMsgCh: + require.Equal(t, msg, rsMsg) + case <-time.After(time.Second): + t.Fail() + } + + // no other relay should have been asked + require.Nil(t, backend.relays[1].submittedMsg) + require.Nil(t, backend.relays[2].submittedMsg) + }) + + t.Run("should submit to relays with matching validator data and drop registrations on next slot", func(t *testing.T) { + backend := newTestRelayAggBackend(3) + + backend.relays[0].gvsVd.GasLimit = 10 + backend.relays[1].gvsVd.GasLimit = 20 + backend.relays[2].gvsVd.GasLimit = 30 + + vd, err := backend.ragg.GetValidatorForSlot(11) + require.NoError(t, err) + require.Equal(t, uint64(10), vd.GasLimit) + + // let the validator registrations finish + time.Sleep(10 * time.Millisecond) + + backend.relays[0].gvsVd.GasLimit = 30 + backend.relays[1].gvsVd.GasLimit = 20 + backend.relays[2].gvsVd.GasLimit = 30 + + // should drop registrations if asked for the next slot + vd, err = backend.ragg.GetValidatorForSlot(12) + require.NoError(t, err) + require.Equal(t, uint64(30), vd.GasLimit) + + time.Sleep(10 * time.Millisecond) + + // should submit to multiple matching relays + backend.relays[0].submittedMsgCh = make(chan *bellatrix.SubmitBlockRequest, 1) + backend.relays[2].submittedMsgCh = make(chan *bellatrix.SubmitBlockRequest, 1) + msg := &bellatrix.SubmitBlockRequest{} + err = backend.ragg.SubmitBlock(msg, ValidatorData{GasLimit: 10}) + require.Error(t, err) + + err = backend.ragg.SubmitBlock(msg, ValidatorData{GasLimit: 30}) + require.NoError(t, err) + + select { + case rsMsg := <-backend.relays[0].submittedMsgCh: + require.Equal(t, msg, rsMsg) + case <-time.After(time.Second): + t.Fail() + } + + select { + case rsMsg := <-backend.relays[2].submittedMsgCh: + require.Equal(t, msg, rsMsg) + case <-time.After(time.Second): + t.Fail() + } + }) +} diff --git a/builder/relay_test.go b/builder/relay_test.go new file mode 100644 index 0000000000..6c36651807 --- /dev/null +++ b/builder/relay_test.go @@ -0,0 +1,128 @@ +package builder + +import ( + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/attestantio/go-eth2-client/spec/bellatrix" + "github.com/gorilla/mux" + "github.com/stretchr/testify/require" +) + +func TestRemoteRelay(t *testing.T) { + r := mux.NewRouter() + var validatorsHandler func(w http.ResponseWriter, r *http.Request) + r.HandleFunc("/relay/v1/builder/validators", func(w http.ResponseWriter, r *http.Request) { validatorsHandler(w, r) }) + + validatorsHandler = func(w http.ResponseWriter, r *http.Request) { + resp := `[{ + "slot": "123", + "entry": { + "message": { + "fee_recipient": "0xabcf8e0d4e9587369b2301d0790347320302cc09", + "gas_limit": "1", + "timestamp": "1", + "pubkey": "0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a" + }, + "signature": "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505" + }}, { + "slot": "155", + "entry": { + "message": { + "fee_recipient": "0xabcf8e0d4e9587369b2301d0790347320302cc10", + "gas_limit": "1", + "timestamp": "1", + "pubkey": "0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a" + }, + "signature": "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505" + } +}]` + + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(resp)) + } + + srv := httptest.NewServer(r) + relay := NewRemoteRelay(RelayConfig{Endpoint: srv.URL, SszEnabled: false, GzipEnabled: false}, nil, false) + relay.validatorsLock.RLock() + vd, found := relay.validatorSlotMap[123] + relay.validatorsLock.RUnlock() + require.True(t, found) + expectedValidator_123 := ValidatorData{ + Pubkey: "0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a", + FeeRecipient: bellatrix.ExecutionAddress{0xab, 0xcf, 0x8e, 0xd, 0x4e, 0x95, 0x87, 0x36, 0x9b, 0x23, 0x1, 0xd0, 0x79, 0x3, 0x47, 0x32, 0x3, 0x2, 0xcc, 0x9}, + GasLimit: uint64(1), + } + require.Equal(t, expectedValidator_123, vd) + + vd, err := relay.GetValidatorForSlot(123) + require.NoError(t, err) + require.Equal(t, expectedValidator_123, vd) + + vd, err = relay.GetValidatorForSlot(124) + require.Error(t, err) + require.Equal(t, vd, ValidatorData{}) + + validatorsRequested := make(chan struct{}) + validatorsHandler = func(w http.ResponseWriter, r *http.Request) { + resp := `[{ + "slot": "155", + "entry": { + "message": { + "fee_recipient": "0xabcf8e0d4e9587369b2301d0790347320302cc10", + "gas_limit": "1", + "timestamp": "1", + "pubkey": "0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a" + }, + "signature": "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505" + }}, { + "slot": "156", + "entry": { + "message": { + "fee_recipient": "0xabcf8e0d4e9587369b2301d0790347320302cc11", + "gas_limit": "1", + "timestamp": "1", + "pubkey": "0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a" + }, + "signature": "0x1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505cc411d61252fb6cb3fa0017b679f8bb2305b26a285fa2737f175668d0dff91cc1b66ac1fb663c9bc59509846d6ec05345bd908eda73e670af888da41af171505" + } +}]` + + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(resp)) + validatorsRequested <- struct{}{} + } + + expectedValidator_155 := ValidatorData{ + Pubkey: "0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a", + FeeRecipient: bellatrix.ExecutionAddress{0xab, 0xcf, 0x8e, 0xd, 0x4e, 0x95, 0x87, 0x36, 0x9b, 0x23, 0x1, 0xd0, 0x79, 0x3, 0x47, 0x32, 0x3, 0x2, 0xcc, 0x10}, + GasLimit: uint64(1), + } + + expectedValidator_156 := ValidatorData{ + Pubkey: "0x93247f2209abcacf57b75a51dafae777f9dd38bc7053d1af526f220a7489a6d3a2753e5f3e8b1cfe39b56f43611df74a", + FeeRecipient: bellatrix.ExecutionAddress{0xab, 0xcf, 0x8e, 0xd, 0x4e, 0x95, 0x87, 0x36, 0x9b, 0x23, 0x1, 0xd0, 0x79, 0x3, 0x47, 0x32, 0x3, 0x2, 0xcc, 0x11}, + GasLimit: uint64(1), + } + + vd, err = relay.GetValidatorForSlot(155) + require.NoError(t, err) + require.Equal(t, expectedValidator_155, vd) + + select { + case <-validatorsRequested: + relay.validatorsLock.RLock() + for i := 0; i < 10 && relay.lastRequestedSlot != 155; i++ { + time.Sleep(time.Millisecond) + } + relay.validatorsLock.RUnlock() + case <-time.After(time.Second): + t.Error("timeout waiting for validator registration request") + } + + vd, err = relay.GetValidatorForSlot(156) + require.NoError(t, err) + require.Equal(t, expectedValidator_156, vd) +} diff --git a/builder/resubmit_utils.go b/builder/resubmit_utils.go new file mode 100644 index 0000000000..4819287e6d --- /dev/null +++ b/builder/resubmit_utils.go @@ -0,0 +1,94 @@ +package builder + +import ( + "context" + "time" + + "github.com/ethereum/go-ethereum/log" + "golang.org/x/time/rate" +) + +// runResubmitLoop checks for update signal and calls submit respecting provided rate limiter and context +func runResubmitLoop(ctx context.Context, limiter *rate.Limiter, updateSignal <-chan struct{}, submit func(), submitTime time.Time) { + if submitTime.IsZero() { + log.Warn("skipping resubmit loop - zero submit time found") + return + } + + waitUntilSubmitTime := func(waitUntil time.Time) (ok bool, err error) { + now := time.Now().UTC() + if waitUntil.UTC().Before(now) { + waitUntil = now + } + sleepTime := waitUntil.UTC().Sub(now.UTC()) + select { + case <-ctx.Done(): + ok = false + case <-time.After(sleepTime): + ok = true + } + return ok && ctx.Err() == nil, ctx.Err() + } + + if canContinue, err := waitUntilSubmitTime(submitTime); !canContinue { + log.Warn("skipping resubmit loop - cannot continue", "error", err) + return + } + + var res *rate.Reservation + for { + select { + case <-ctx.Done(): + return + case <-updateSignal: + // runBuildingJob is example caller that uses updateSignal channel via block hook that sends signal to + // represent submissions that increase block profit + + res = limiter.Reserve() + if !res.OK() { + log.Warn("resubmit loop failed to make limiter reservation") + return + } + + // check if we could make submission before context ctxDeadline + if ctxDeadline, ok := ctx.Deadline(); ok { + delayDeadline := time.Now().Add(res.Delay()) + if delayDeadline.After(ctxDeadline) { + res.Cancel() + return + } + } + + delay := res.Delay() + if delay == 0 { + submit() + continue + } + + t := time.NewTimer(delay) + select { + case <-t.C: + submit() + continue + case <-ctx.Done(): + res.Cancel() + t.Stop() + return + } + } + } +} + +// runRetryLoop calls retry periodically with the provided interval respecting context cancellation +func runRetryLoop(ctx context.Context, interval time.Duration, retry func()) { + t := time.NewTicker(interval) + defer t.Stop() + for { + select { + case <-ctx.Done(): + return + case <-t.C: + retry() + } + } +} diff --git a/builder/resubmit_utils_test.go b/builder/resubmit_utils_test.go new file mode 100644 index 0000000000..602daef99a --- /dev/null +++ b/builder/resubmit_utils_test.go @@ -0,0 +1,76 @@ +package builder + +import ( + "context" + "math/rand" + "sort" + "sync" + "testing" + "time" + + "golang.org/x/time/rate" +) + +type submission struct { + t time.Time + v int +} + +func TestResubmitUtils(t *testing.T) { + const ( + totalTime = time.Second + rateLimitTime = 100 * time.Millisecond + resubmitInterval = 10 * time.Millisecond + ) + + ctx, cancel := context.WithTimeout(context.Background(), totalTime) + defer cancel() + limiter := rate.NewLimiter(rate.Every(rateLimitTime), 1) + + var ( + signal = make(chan struct{}, 1) + subMu sync.Mutex + subLast int + subBest int + subAll []submission + ) + + go runResubmitLoop(ctx, limiter, signal, func() { + subMu.Lock() + defer subMu.Unlock() + + if subBest > subLast { + subAll = append(subAll, submission{time.Now(), subBest}) + subLast = subBest + } + }, time.Now()) + + runRetryLoop(ctx, resubmitInterval, func() { + subMu.Lock() + defer subMu.Unlock() + + value := rand.Int() + if value > subBest { + subBest = value + + select { + case signal <- struct{}{}: + default: + } + } + }) + + sorted := sort.SliceIsSorted(subAll, func(i, j int) bool { + return subAll[i].v < subAll[j].v + }) + if !sorted { + t.Error("submissions are not sorted") + } + + for i := 0; i < len(subAll)-1; i++ { + interval := subAll[i+1].t.Sub(subAll[i].t) + if interval+10*time.Millisecond < rateLimitTime { + t.Errorf("submissions are not rate limited: interval %s, limit %s", interval, rateLimitTime) + } + } +} diff --git a/builder/service.go b/builder/service.go new file mode 100644 index 0000000000..23f57c71cb --- /dev/null +++ b/builder/service.go @@ -0,0 +1,279 @@ +package builder + +import ( + "errors" + "fmt" + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/ethereum/go-ethereum/eth" + "net/http" + "strconv" + "strings" + "time" + + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/types" + blockvalidation "github.com/ethereum/go-ethereum/eth/block-validation" + "github.com/ethereum/go-ethereum/flashbotsextra" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/node" + "github.com/ethereum/go-ethereum/rpc" + "github.com/flashbots/go-boost-utils/bls" + "github.com/flashbots/go-utils/httplogger" + "github.com/gorilla/mux" + "golang.org/x/time/rate" +) + +const ( + _PathStatus = "/eth/v1/builder/status" + _PathRegisterValidator = "/eth/v1/builder/validators" + _PathGetHeader = "/eth/v1/builder/header/{slot:[0-9]+}/{parent_hash:0x[a-fA-F0-9]+}/{pubkey:0x[a-fA-F0-9]+}" + _PathGetPayload = "/eth/v1/builder/blinded_blocks" +) + +type Service struct { + srv *http.Server + builder IBuilder +} + +func (s *Service) Start() error { + if s.srv != nil { + log.Info("Service started") + go s.srv.ListenAndServe() + } + + s.builder.Start() + + return nil +} + +func (s *Service) Stop() error { + if s.srv != nil { + s.srv.Close() + } + s.builder.Stop() + return nil +} + +func (s *Service) PayloadAttributes(payloadAttributes *types.BuilderPayloadAttributes) error { + return s.builder.OnPayloadAttribute(payloadAttributes) +} + +func getRouter(localRelay *LocalRelay) http.Handler { + router := mux.NewRouter() + + // Add routes + router.HandleFunc("/", localRelay.handleIndex).Methods(http.MethodGet) + router.HandleFunc(_PathStatus, localRelay.handleStatus).Methods(http.MethodGet) + router.HandleFunc(_PathRegisterValidator, localRelay.handleRegisterValidator).Methods(http.MethodPost) + router.HandleFunc(_PathGetHeader, localRelay.handleGetHeader).Methods(http.MethodGet) + router.HandleFunc(_PathGetPayload, localRelay.handleGetPayload).Methods(http.MethodPost) + + // Add logging and return router + loggedRouter := httplogger.LoggingMiddleware(router) + return loggedRouter +} + +func getRelayConfig(endpoint string) (RelayConfig, error) { + configs := strings.Split(endpoint, ";") + if len(configs) == 0 { + return RelayConfig{}, fmt.Errorf("empty relay endpoint %s", endpoint) + } + relayUrl := configs[0] + // relay endpoint is configurated in the format URL;ssz=;gzip= + // if any of them are missing, we default the config value to false + var sszEnabled, gzipEnabled bool + var err error + + for _, config := range configs { + if strings.HasPrefix(config, "ssz=") { + sszEnabled, err = strconv.ParseBool(config[4:]) + if err != nil { + log.Info("invalid ssz config for relay", "endpoint", endpoint, "err", err) + } + } else if strings.HasPrefix(config, "gzip=") { + gzipEnabled, err = strconv.ParseBool(config[5:]) + if err != nil { + log.Info("invalid gzip config for relay", "endpoint", endpoint, "err", err) + } + } + } + return RelayConfig{ + Endpoint: relayUrl, + SszEnabled: sszEnabled, + GzipEnabled: gzipEnabled, + }, nil +} + +func NewService(listenAddr string, localRelay *LocalRelay, builder IBuilder) *Service { + var srv *http.Server + if localRelay != nil { + srv = &http.Server{ + Addr: listenAddr, + Handler: getRouter(localRelay), + /* + ReadTimeout: + ReadHeaderTimeout: + WriteTimeout: + IdleTimeout: + */ + } + } + + return &Service{ + srv: srv, + builder: builder, + } +} + +func Register(stack *node.Node, backend *eth.Ethereum, cfg *Config) error { + envBuilderSkBytes, err := hexutil.Decode(cfg.BuilderSecretKey) + if err != nil { + return errors.New("incorrect builder API secret key provided") + } + + var beaconClient IBeaconClient = &NilBeaconClient{} + + var localRelay *LocalRelay + builderSigningDomain, proposerSigningDomain := phase0.Domain{}, phase0.Domain{} + + if cfg.EnableLocalRelay { + envRelaySkBytes, err := hexutil.Decode(cfg.RelaySecretKey) + if err != nil { + return errors.New("incorrect builder API secret key provided") + } + + relaySk, err := bls.SecretKeyFromBytes(envRelaySkBytes[:]) + if err != nil { + return errors.New("incorrect builder API secret key provided") + } + + localRelay, err = NewLocalRelay(relaySk, beaconClient, builderSigningDomain, proposerSigningDomain, ForkData{cfg.GenesisForkVersion, cfg.BellatrixForkVersion, cfg.GenesisValidatorsRoot}, cfg.EnableValidatorChecks) + if err != nil { + return fmt.Errorf("failed to create local relay: %w", err) + } + } + + var relay IRelay + if cfg.RemoteRelayEndpoint != "" { + relayConfig, err := getRelayConfig(cfg.RemoteRelayEndpoint) + if err != nil { + return fmt.Errorf("invalid remote relay endpoint: %w", err) + } + relay = NewRemoteRelay(relayConfig, localRelay, cfg.EnableCancellations) + } else if localRelay != nil { + relay = localRelay + } else { + return errors.New("neither local nor remote relay specified") + } + + if len(cfg.SecondaryRemoteRelayEndpoints) > 0 && !(len(cfg.SecondaryRemoteRelayEndpoints) == 1 && cfg.SecondaryRemoteRelayEndpoints[0] == "") { + secondaryRelays := make([]IRelay, len(cfg.SecondaryRemoteRelayEndpoints)) + for i, endpoint := range cfg.SecondaryRemoteRelayEndpoints { + relayConfig, err := getRelayConfig(endpoint) + if err != nil { + return fmt.Errorf("invalid secondary remote relay endpoint: %w", err) + } + secondaryRelays[i] = NewRemoteRelay(relayConfig, nil, cfg.EnableCancellations) + } + relay = NewRemoteRelayAggregator(relay, secondaryRelays) + } + + var validator *blockvalidation.BlockValidationAPI + if cfg.DryRun { + var accessVerifier *blockvalidation.AccessVerifier + if cfg.ValidationBlocklist != "" { + accessVerifier, err = blockvalidation.NewAccessVerifierFromFile(cfg.ValidationBlocklist) + if err != nil { + return fmt.Errorf("failed to load validation blocklist %w", err) + } + } + validator = blockvalidation.NewBlockValidationAPI(backend, accessVerifier, cfg.ValidationUseCoinbaseDiff) + } + + // Set up builder rate limiter based on environment variables or CLI flags. + // Builder rate limit parameters are flags.BuilderRateLimitDuration and flags.BuilderRateLimitMaxBurst + duration, err := time.ParseDuration(cfg.BuilderRateLimitDuration) + if err != nil { + return fmt.Errorf("error parsing builder rate limit duration - %w", err) + } + + // BuilderRateLimitMaxBurst is set to builder.RateLimitBurstDefault by default if not specified + limiter := rate.NewLimiter(rate.Every(duration), cfg.BuilderRateLimitMaxBurst) + + var builderRateLimitInterval time.Duration + if cfg.BuilderRateLimitResubmitInterval != "" { + d, err := time.ParseDuration(cfg.BuilderRateLimitResubmitInterval) + if err != nil { + return fmt.Errorf("error parsing builder rate limit resubmit interval - %v", err) + } + builderRateLimitInterval = d + } else { + builderRateLimitInterval = RateLimitIntervalDefault + } + + var submissionOffset time.Duration + if offset := cfg.BuilderSubmissionOffset; offset != 0 { + if offset < 0 { + return fmt.Errorf("builder submission offset must be positive") + } else if uint64(offset.Seconds()) > cfg.SecondsInSlot { + return fmt.Errorf("builder submission offset must be less than seconds in slot") + } + submissionOffset = offset + } else { + submissionOffset = SubmissionOffsetFromEndOfSlotSecondsDefault + } + + // TODO: move to proper flags + var ds flashbotsextra.IDatabaseService = flashbotsextra.NilDbService{} + + // Bundle fetcher + // TODO: can I ignore BundleFetcher? + //if !cfg.DisableBundleFetcher { + // mevBundleCh := make(chan []types.MevBundle) + // blockNumCh := make(chan int64) + // bundleFetcher := flashbotsextra.NewBundleFetcher(backend, ds, blockNumCh, mevBundleCh, true) + // backend.RegisterBundleFetcher(bundleFetcher) + // go bundleFetcher.Run() + //} + + ethereumService := NewEthereumService(backend) + + builderSk, err := bls.SecretKeyFromBytes(envBuilderSkBytes[:]) + if err != nil { + return errors.New("incorrect builder API secret key provided") + } + proposerPubKey, err := bls.PublicKeyFromBytes(cfg.ProposerPubkey) + + builderArgs := CliqueBuilderArgs{ + sk: builderSk, + ds: ds, + eth: ethereumService, + relay: relay, + proposerPubkey: proposerPubKey, + builderSigningDomain: builderSigningDomain, + builderBlockResubmitInterval: builderRateLimitInterval, + submissionOffsetFromEndOfSlot: submissionOffset, + limiter: limiter, + validator: validator, + } + + builderBackend, err := NewCliqueBuilder(builderArgs) + if err != nil { + return fmt.Errorf("failed to create builder backend: %w", err) + } + builderService := NewService(cfg.ListenAddr, localRelay, builderBackend) + + stack.RegisterAPIs([]rpc.API{ + { + Namespace: "builder", + Version: "1.0", + Service: builderService, + Public: true, + Authenticated: true, + }, + }) + + stack.RegisterLifecycle(builderService) + + return nil +} diff --git a/builder/utils.go b/builder/utils.go new file mode 100644 index 0000000000..284285cf4e --- /dev/null +++ b/builder/utils.go @@ -0,0 +1,119 @@ +package builder + +import ( + "bytes" + "compress/gzip" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" +) + +var errHTTPErrorResponse = errors.New("HTTP error response") + +// SendSSZRequest is a request to send SSZ data to a remote relay. +func SendSSZRequest(ctx context.Context, client http.Client, method, url string, payload []byte, useGzip bool) (code int, err error) { + var req *http.Request + + reader := bytes.NewReader(payload) + + if useGzip { + // Create a new gzip writer + var buf bytes.Buffer + gzipWriter := gzip.NewWriter(&buf) + + // Write the payload to the gzip writer + _, err = reader.WriteTo(gzipWriter) + if err != nil { + return 0, fmt.Errorf("error writing payload to gzip writer: %w", err) + } + + // Flush and close the gzip writer to finalize the compressed data + err = gzipWriter.Close() + if err != nil { + return 0, fmt.Errorf("error closing gzip writer: %w", err) + } + + req, err = http.NewRequest(http.MethodPost, url, &buf) + if err != nil { + return 0, fmt.Errorf("error creating request: %w", err) + } + req.Header.Add("Content-Encoding", "gzip") + } else { + req, err = http.NewRequest(http.MethodPost, url, reader) + if err != nil { + return 0, fmt.Errorf("error creating request: %w", err) + } + } + + req.Header.Add("Content-Type", "application/octet-stream") + resp, err := http.DefaultClient.Do(req) + if err != nil { + return 0, fmt.Errorf("error sending request: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode > 299 { + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + return resp.StatusCode, fmt.Errorf("could not read error response body for status code %d: %w", resp.StatusCode, err) + } + return resp.StatusCode, fmt.Errorf("HTTP error response: %d / %s", resp.StatusCode, string(bodyBytes)) + } + return resp.StatusCode, nil +} + +// SendHTTPRequest - prepare and send HTTP request, marshaling the payload if any, and decoding the response if dst is set +func SendHTTPRequest(ctx context.Context, client http.Client, method, url string, payload, dst any) (code int, err error) { + var req *http.Request + + if payload == nil { + req, err = http.NewRequestWithContext(ctx, method, url, nil) + } else { + payloadBytes, err2 := json.Marshal(payload) + if err2 != nil { + return 0, fmt.Errorf("could not marshal request: %w", err2) + } + req, err = http.NewRequestWithContext(ctx, method, url, bytes.NewReader(payloadBytes)) + + // Set headers + req.Header.Add("Content-Type", "application/json") + } + if err != nil { + return 0, fmt.Errorf("could not prepare request: %w", err) + } + + // Execute request + resp, err := client.Do(req) + if err != nil { + return 0, err + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusNoContent { + return resp.StatusCode, nil + } + + if resp.StatusCode > 299 { + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + return resp.StatusCode, fmt.Errorf("could not read error response body for status code %d: %w", resp.StatusCode, err) + } + return resp.StatusCode, fmt.Errorf("%w: %d / %s", errHTTPErrorResponse, resp.StatusCode, string(bodyBytes)) + } + + if dst != nil { + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + return resp.StatusCode, fmt.Errorf("could not read response body: %w", err) + } + + if err := json.Unmarshal(bodyBytes, dst); err != nil { + return resp.StatusCode, fmt.Errorf("could not unmarshal response %s: %w", string(bodyBytes), err) + } + } + + return resp.StatusCode, nil +} diff --git a/builder/validator.go b/builder/validator.go new file mode 100644 index 0000000000..9adcc51493 --- /dev/null +++ b/builder/validator.go @@ -0,0 +1,55 @@ +package builder + +import ( + "errors" + "time" + + apiv1 "github.com/attestantio/go-builder-client/api/v1" + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/flashbots/go-boost-utils/bls" + "github.com/flashbots/go-boost-utils/ssz" + "github.com/flashbots/go-boost-utils/utils" +) + +type ValidatorPrivateData struct { + sk *bls.SecretKey + Pk hexutil.Bytes +} + +func NewRandomValidator() *ValidatorPrivateData { + sk, pk, err := bls.GenerateNewKeypair() + if err != nil { + return nil + } + return &ValidatorPrivateData{sk, bls.PublicKeyToBytes(pk)} +} + +func (v *ValidatorPrivateData) Sign(msg ssz.ObjWithHashTreeRoot, d phase0.Domain) (phase0.BLSSignature, error) { + return ssz.SignMessage(msg, d, v.sk) +} + +func (v *ValidatorPrivateData) PrepareRegistrationMessage(feeRecipientHex string) (apiv1.SignedValidatorRegistration, error) { + address, err := utils.HexToAddress(feeRecipientHex) + if err != nil { + return apiv1.SignedValidatorRegistration{}, err + } + + if len(v.Pk) != 48 { + return apiv1.SignedValidatorRegistration{}, errors.New("invalid public key") + } + pubkey := phase0.BLSPubKey{} + copy(pubkey[:], v.Pk) + + msg := &apiv1.ValidatorRegistration{ + FeeRecipient: address, + GasLimit: 1000, + Timestamp: time.Now(), + Pubkey: pubkey, + } + signature, err := v.Sign(msg, ssz.DomainBuilder) + if err != nil { + return apiv1.SignedValidatorRegistration{}, err + } + return apiv1.SignedValidatorRegistration{Message: msg, Signature: signature}, nil +} diff --git a/core/blockchain.go b/core/blockchain.go index fd50d19042..1aa8524279 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -48,6 +48,7 @@ import ( "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/core/state/snapshot" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/utils" "github.com/ethereum/go-ethereum/core/vm" "github.com/ethereum/go-ethereum/eth/downloader/whitelist" "github.com/ethereum/go-ethereum/ethdb" @@ -3124,6 +3125,134 @@ func (bc *BlockChain) SetBlockValidatorAndProcessorForTesting(v Validator, p Pro bc.processor = p } +// ValidatePayload validates the payload of the block. +// It returns nil if the payload is valid, otherwise it returns an error. +// - `useBalanceDiffProfit` if set to false, proposer payment is assumed to be in the last transaction of the block +// otherwise we use proposer balance changes after the block to calculate proposer payment (see details in the code) +func (bc *BlockChain) ValidatePayload(block *types.Block, feeRecipient common.Address, expectedProfit *big.Int, registeredGasLimit uint64, vmConfig vm.Config, useBalanceDiffProfit bool) error { + ctx, _ := context.WithCancel(context.Background()) + header := block.Header() + if err := bc.engine.VerifyHeader(bc, header, true); err != nil { + return err + } + + current := bc.CurrentBlock() + reorg, err := bc.forker.ReorgNeeded(current, header) + if err == nil && reorg { + return errors.New("block requires a reorg") + } + + parent := bc.GetHeader(block.ParentHash(), block.NumberU64()-1) + if parent == nil { + return errors.New("parent not found") + } + + calculatedGasLimit := utils.CalcGasLimit(parent.GasLimit, registeredGasLimit) + if calculatedGasLimit != header.GasLimit { + return errors.New("incorrect gas limit set") + } + + statedb, err := bc.StateAt(parent.Root) + if err != nil { + return err + } + + // The chain importer is starting and stopping trie prefetchers. If a bad + // block or other error is hit however, an early return may not properly + // terminate the background threads. This defer ensures that we clean up + // and dangling prefetcher, without defering each and holding on live refs. + defer statedb.StopPrefetcher() + + feeRecipientBalanceBefore := new(big.Int).Set(statedb.GetBalance(feeRecipient)) + + receipts, _, usedGas, err := bc.processor.Process(block, statedb, vmConfig, ctx) + if err != nil { + return err + } + + feeRecipientBalanceDelta := new(big.Int).Set(statedb.GetBalance(feeRecipient)) + feeRecipientBalanceDelta.Sub(feeRecipientBalanceDelta, feeRecipientBalanceBefore) + + if bc.Config().IsShanghai(header.Time) { + if header.WithdrawalsHash == nil { + return fmt.Errorf("withdrawals hash is missing") + } + // withdrawals hash and withdrawals validated later in ValidateBody + } else { + if header.WithdrawalsHash != nil { + return fmt.Errorf("withdrawals hash present before shanghai") + } + if block.Withdrawals() != nil { + return fmt.Errorf("withdrawals list present in block body before shanghai") + } + } + + if err := bc.validator.ValidateBody(block); err != nil { + return err + } + + if err := bc.validator.ValidateState(block, statedb, receipts, usedGas); err != nil { + return err + } + + // Validate proposer payment + + if useBalanceDiffProfit { + if feeRecipientBalanceDelta.Cmp(expectedProfit) >= 0 { + if feeRecipientBalanceDelta.Cmp(expectedProfit) > 0 { + log.Warn("builder claimed profit is lower than calculated profit", "expected", expectedProfit, "actual", feeRecipientBalanceDelta) + } + return nil + } + log.Warn("proposer payment not enough, trying last tx payment validation", "expected", expectedProfit, "actual", feeRecipientBalanceDelta) + } + + if len(receipts) == 0 { + return errors.New("no proposer payment receipt") + } + + lastReceipt := receipts[len(receipts)-1] + if lastReceipt.Status != types.ReceiptStatusSuccessful { + return errors.New("proposer payment not successful") + } + txIndex := lastReceipt.TransactionIndex + if txIndex+1 != uint(len(block.Transactions())) { + return fmt.Errorf("proposer payment index not last transaction in the block (%d of %d)", txIndex, len(block.Transactions())-1) + } + + paymentTx := block.Transaction(lastReceipt.TxHash) + if paymentTx == nil { + return errors.New("payment tx not in the block") + } + + paymentTo := paymentTx.To() + if paymentTo == nil || *paymentTo != feeRecipient { + return fmt.Errorf("payment tx not to the proposers fee recipient (%v)", paymentTo) + } + + if paymentTx.Value().Cmp(expectedProfit) != 0 { + return fmt.Errorf("inaccurate payment %s, expected %s", paymentTx.Value().String(), expectedProfit.String()) + } + + if len(paymentTx.Data()) != 0 { + return fmt.Errorf("malformed proposer payment, contains calldata") + } + + if paymentTx.GasPrice().Cmp(block.BaseFee()) != 0 { + return fmt.Errorf("malformed proposer payment, gas price not equal to base fee") + } + + if paymentTx.GasTipCap().Cmp(block.BaseFee()) != 0 && paymentTx.GasTipCap().Sign() != 0 { + return fmt.Errorf("malformed proposer payment, unexpected gas tip cap") + } + + if paymentTx.GasFeeCap().Cmp(block.BaseFee()) != 0 { + return fmt.Errorf("malformed proposer payment, unexpected gas fee cap") + } + + return nil +} + // SetTrieFlushInterval configures how often in-memory tries are persisted to disk. // The interval is in terms of block processing time, not wall clock. // It is thread-safe and can be called repeatedly without side effects. diff --git a/core/types/builder.go b/core/types/builder.go new file mode 100644 index 0000000000..d31da82fdb --- /dev/null +++ b/core/types/builder.go @@ -0,0 +1,33 @@ +package types + +import ( + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "golang.org/x/exp/slices" +) + +type BuilderPayloadAttributes struct { + Timestamp hexutil.Uint64 `json:"timestamp"` + Random common.Hash `json:"prevRandao"` + SuggestedFeeRecipient common.Address `json:"suggestedFeeRecipient,omitempty"` + Slot uint64 `json:"slot"` + HeadHash common.Hash `json:"blockHash"` + Withdrawals Withdrawals `json:"withdrawals"` + GasLimit uint64 +} + +func (attrs *BuilderPayloadAttributes) Equal(other *BuilderPayloadAttributes) bool { + if attrs.Timestamp != other.Timestamp || + attrs.Random != other.Random || + attrs.SuggestedFeeRecipient != other.SuggestedFeeRecipient || + attrs.Slot != other.Slot || + attrs.HeadHash != other.HeadHash || + attrs.GasLimit != other.GasLimit { + return false + } + + if !slices.Equal(attrs.Withdrawals, other.Withdrawals) { + return false + } + return true +} diff --git a/core/utils/gas_limit.go b/core/utils/gas_limit.go new file mode 100644 index 0000000000..8be0a6a43e --- /dev/null +++ b/core/utils/gas_limit.go @@ -0,0 +1,29 @@ +package utils + +import "github.com/ethereum/go-ethereum/params" + +// CalcGasLimit computes the gas limit of the next block after parent. It aims +// to keep the baseline gas close to the provided target, and increase it towards +// the target if the baseline gas is lower. +func CalcGasLimit(parentGasLimit, desiredLimit uint64) uint64 { + delta := parentGasLimit/params.GasLimitBoundDivisor - 1 + limit := parentGasLimit + if desiredLimit < params.MinGasLimit { + desiredLimit = params.MinGasLimit + } + // If we're outside our allowed gas range, we try to hone towards them + if limit < desiredLimit { + limit = parentGasLimit + delta + if limit > desiredLimit { + limit = desiredLimit + } + return limit + } + if limit > desiredLimit { + limit = parentGasLimit - delta + if limit < desiredLimit { + limit = desiredLimit + } + } + return limit +} diff --git a/eth/block-validation/api.go b/eth/block-validation/api.go new file mode 100644 index 0000000000..43e182bf8f --- /dev/null +++ b/eth/block-validation/api.go @@ -0,0 +1,294 @@ +package blockvalidation + +import ( + "encoding/json" + "errors" + "fmt" + "github.com/ethereum/go-ethereum/eth" + "math/big" + "os" + + bellatrixapi "github.com/attestantio/go-builder-client/api/bellatrix" + capellaapi "github.com/attestantio/go-builder-client/api/capella" + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/ethereum/go-ethereum/beacon/engine" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/eth/tracers/logger" + "github.com/ethereum/go-ethereum/log" + "github.com/ethereum/go-ethereum/node" + "github.com/ethereum/go-ethereum/rpc" +) + +type BlacklistedAddresses []common.Address + +type AccessVerifier struct { + blacklistedAddresses map[common.Address]struct{} +} + +func (a *AccessVerifier) verifyTraces(tracer *logger.AccessListTracer) error { + log.Trace("x", "tracer.AccessList()", tracer.AccessList()) + for _, accessTuple := range tracer.AccessList() { + // TODO: should we ignore common.Address{}? + if _, found := a.blacklistedAddresses[accessTuple.Address]; found { + log.Info("bundle accesses blacklisted address", "address", accessTuple.Address) + return fmt.Errorf("blacklisted address %s in execution trace", accessTuple.Address.String()) + } + } + + return nil +} + +func (a *AccessVerifier) isBlacklisted(addr common.Address) error { + if _, present := a.blacklistedAddresses[addr]; present { + return fmt.Errorf("transaction from blacklisted address %s", addr.String()) + } + return nil +} + +func (a *AccessVerifier) verifyTransactions(signer types.Signer, txs types.Transactions) error { + for _, tx := range txs { + from, err := types.Sender(signer, tx) + if err == nil { + if _, present := a.blacklistedAddresses[from]; present { + return fmt.Errorf("transaction from blacklisted address %s", from.String()) + } + } + to := tx.To() + if to != nil { + if _, present := a.blacklistedAddresses[*to]; present { + return fmt.Errorf("transaction to blacklisted address %s", to.String()) + } + } + } + return nil +} + +func NewAccessVerifierFromFile(path string) (*AccessVerifier, error) { + bytes, err := os.ReadFile(path) + if err != nil { + return nil, err + } + + var ba BlacklistedAddresses + if err := json.Unmarshal(bytes, &ba); err != nil { + return nil, err + } + + blacklistedAddresses := make(map[common.Address]struct{}, len(ba)) + for _, address := range ba { + blacklistedAddresses[address] = struct{}{} + } + + return &AccessVerifier{ + blacklistedAddresses: blacklistedAddresses, + }, nil +} + +type BlockValidationConfig struct { + BlacklistSourceFilePath string + // If set to true, proposer payment is calculated as a balance difference of the fee recipient. + UseBalanceDiffProfit bool +} + +// Register adds catalyst APIs to the full node. +func Register(stack *node.Node, backend *eth.Ethereum, cfg BlockValidationConfig) error { + var accessVerifier *AccessVerifier + if cfg.BlacklistSourceFilePath != "" { + var err error + accessVerifier, err = NewAccessVerifierFromFile(cfg.BlacklistSourceFilePath) + if err != nil { + return err + } + } + + stack.RegisterAPIs([]rpc.API{ + { + Namespace: "flashbots", + Service: NewBlockValidationAPI(backend, accessVerifier, cfg.UseBalanceDiffProfit), + }, + }) + return nil +} + +type BlockValidationAPI struct { + eth *eth.Ethereum + accessVerifier *AccessVerifier + // If set to true, proposer payment is calculated as a balance difference of the fee recipient. + useBalanceDiffProfit bool +} + +// NewConsensusAPI creates a new consensus api for the given backend. +// The underlying blockchain needs to have a valid terminal total difficulty set. +func NewBlockValidationAPI(eth *eth.Ethereum, accessVerifier *AccessVerifier, useBalanceDiffProfit bool) *BlockValidationAPI { + return &BlockValidationAPI{ + eth: eth, + accessVerifier: accessVerifier, + useBalanceDiffProfit: useBalanceDiffProfit, + } +} + +type BuilderBlockValidationRequest struct { + bellatrixapi.SubmitBlockRequest + RegisteredGasLimit uint64 `json:"registered_gas_limit,string"` +} + +func (api *BlockValidationAPI) ValidateBuilderSubmissionV1(params *BuilderBlockValidationRequest) error { + // TODO: fuzztest, make sure the validation is sound + // TODO: handle context! + + if params.ExecutionPayload == nil { + return errors.New("nil execution payload") + } + payload := params.ExecutionPayload + block, err := engine.ExecutionPayloadToBlock(payload) + if err != nil { + return err + } + + if params.Message.ParentHash != phase0.Hash32(block.ParentHash()) { + return fmt.Errorf("incorrect ParentHash %s, expected %s", params.Message.ParentHash.String(), block.ParentHash().String()) + } + + if params.Message.BlockHash != phase0.Hash32(block.Hash()) { + return fmt.Errorf("incorrect BlockHash %s, expected %s", params.Message.BlockHash.String(), block.Hash().String()) + } + + if params.Message.GasLimit != block.GasLimit() { + return fmt.Errorf("incorrect GasLimit %d, expected %d", params.Message.GasLimit, block.GasLimit()) + } + + if params.Message.GasUsed != block.GasUsed() { + return fmt.Errorf("incorrect GasUsed %d, expected %d", params.Message.GasUsed, block.GasUsed()) + } + + feeRecipient := common.BytesToAddress(params.Message.ProposerFeeRecipient[:]) + expectedProfit := params.Message.Value.ToBig() + + var vmconfig vm.Config + var tracer *logger.AccessListTracer = nil + //if api.accessVerifier != nil { + // if err := api.accessVerifier.isBlacklisted(block.Coinbase()); err != nil { + // return err + // } + // if err := api.accessVerifier.isBlacklisted(feeRecipient); err != nil { + // return err + // } + // if err := api.accessVerifier.verifyTransactions(types.LatestSigner(api.eth.BlockChain().Config()), block.Transactions()); err != nil { + // return err + // } + // isPostMerge := true // the call is PoS-native + // timestamp := params.SubmitBlockRequest.ExecutionPayload.Timestamp + // precompiles := vm.ActivePrecompiles(api.eth.APIBackend.ChainConfig().Rules(new(big.Int).SetUint64(params.ExecutionPayload.BlockNumber), isPostMerge, timestamp)) + // tracer = logger.NewAccessListTracer(nil, common.Address{}, common.Address{}, precompiles) + // vmconfig = vm.Config{Tracer: tracer} + //} + + err = api.eth.BlockChain().ValidatePayload(block, feeRecipient, expectedProfit, params.RegisteredGasLimit, vmconfig, api.useBalanceDiffProfit) + if err != nil { + log.Error("invalid payload", "hash", payload.BlockHash.String(), "number", payload.BlockNumber, "parentHash", payload.ParentHash.String(), "err", err) + return err + } + + if api.accessVerifier != nil && tracer != nil { + if err := api.accessVerifier.verifyTraces(tracer); err != nil { + return err + } + } + + log.Info("validated block", "hash", block.Hash(), "number", block.NumberU64(), "parentHash", block.ParentHash()) + return nil +} + +type BuilderBlockValidationRequestV2 struct { + capellaapi.SubmitBlockRequest + RegisteredGasLimit uint64 `json:"registered_gas_limit,string"` + WithdrawalsRoot common.Hash `json:"withdrawals_root"` +} + +func (r *BuilderBlockValidationRequestV2) UnmarshalJSON(data []byte) error { + params := &struct { + RegisteredGasLimit uint64 `json:"registered_gas_limit,string"` + WithdrawalsRoot common.Hash `json:"withdrawals_root"` + }{} + err := json.Unmarshal(data, params) + if err != nil { + return err + } + r.RegisteredGasLimit = params.RegisteredGasLimit + r.WithdrawalsRoot = params.WithdrawalsRoot + + blockRequest := new(capellaapi.SubmitBlockRequest) + err = json.Unmarshal(data, &blockRequest) + if err != nil { + return err + } + r.SubmitBlockRequest = *blockRequest + return nil +} + +func (api *BlockValidationAPI) ValidateBuilderSubmissionV2(params *BuilderBlockValidationRequestV2) error { + // TODO: fuzztest, make sure the validation is sound + // TODO: handle context! + if params.ExecutionPayload == nil { + return errors.New("nil execution payload") + } + payload := params.ExecutionPayload + block, err := engine.ExecutionPayloadV2ToBlock(payload) + if err != nil { + return err + } + + if params.Message.ParentHash != phase0.Hash32(block.ParentHash()) { + return fmt.Errorf("incorrect ParentHash %s, expected %s", params.Message.ParentHash.String(), block.ParentHash().String()) + } + + if params.Message.BlockHash != phase0.Hash32(block.Hash()) { + return fmt.Errorf("incorrect BlockHash %s, expected %s", params.Message.BlockHash.String(), block.Hash().String()) + } + + if params.Message.GasLimit != block.GasLimit() { + return fmt.Errorf("incorrect GasLimit %d, expected %d", params.Message.GasLimit, block.GasLimit()) + } + + if params.Message.GasUsed != block.GasUsed() { + return fmt.Errorf("incorrect GasUsed %d, expected %d", params.Message.GasUsed, block.GasUsed()) + } + + feeRecipient := common.BytesToAddress(params.Message.ProposerFeeRecipient[:]) + expectedProfit := params.Message.Value.ToBig() + + var vmconfig vm.Config + var tracer *logger.AccessListTracer = nil + if api.accessVerifier != nil { + if err := api.accessVerifier.isBlacklisted(block.Coinbase()); err != nil { + return err + } + if err := api.accessVerifier.isBlacklisted(feeRecipient); err != nil { + return err + } + if err := api.accessVerifier.verifyTransactions(types.LatestSigner(api.eth.BlockChain().Config()), block.Transactions()); err != nil { + return err + } + isPostMerge := true // the call is PoS-native + precompiles := vm.ActivePrecompiles(api.eth.APIBackend.ChainConfig().Rules(new(big.Int).SetUint64(params.ExecutionPayload.BlockNumber), isPostMerge, params.ExecutionPayload.Timestamp)) + tracer = logger.NewAccessListTracer(nil, common.Address{}, common.Address{}, precompiles) + vmconfig = vm.Config{Tracer: tracer} + } + + err = api.eth.BlockChain().ValidatePayload(block, feeRecipient, expectedProfit, params.RegisteredGasLimit, vmconfig, api.useBalanceDiffProfit) + if err != nil { + log.Error("invalid payload", "hash", payload.BlockHash.String(), "number", payload.BlockNumber, "parentHash", payload.ParentHash.String(), "err", err) + return err + } + + if api.accessVerifier != nil && tracer != nil { + if err := api.accessVerifier.verifyTraces(tracer); err != nil { + return err + } + } + + log.Info("validated block", "hash", block.Hash(), "number", block.NumberU64(), "parentHash", block.ParentHash()) + return nil +} diff --git a/eth/block-validation/api_test.go b/eth/block-validation/api_test.go new file mode 100644 index 0000000000..5a925f4316 --- /dev/null +++ b/eth/block-validation/api_test.go @@ -0,0 +1,784 @@ +package blockvalidation + +import ( + "encoding/json" + "errors" + "fmt" + "math/big" + "os" + "testing" + "time" + + bellatrixapi "github.com/attestantio/go-builder-client/api/bellatrix" + capellaapi "github.com/attestantio/go-builder-client/api/capella" + apiv1 "github.com/attestantio/go-builder-client/api/v1" + "github.com/attestantio/go-eth2-client/spec/bellatrix" + "github.com/attestantio/go-eth2-client/spec/capella" + "github.com/attestantio/go-eth2-client/spec/phase0" + "github.com/ethereum/go-ethereum/beacon/engine" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/consensus/ethash" + "github.com/ethereum/go-ethereum/consensus/misc" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/rawdb" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/eth" + "github.com/ethereum/go-ethereum/eth/downloader" + "github.com/ethereum/go-ethereum/eth/ethconfig" + "github.com/ethereum/go-ethereum/eth/tracers/logger" + "github.com/ethereum/go-ethereum/miner" + "github.com/ethereum/go-ethereum/node" + "github.com/ethereum/go-ethereum/p2p" + "github.com/ethereum/go-ethereum/params" + "github.com/ethereum/go-ethereum/trie" + "github.com/holiman/uint256" + "github.com/stretchr/testify/require" + + boostTypes "github.com/flashbots/go-boost-utils/types" +) + +/* Based on catalyst API tests */ + +var ( + // testKey is a private key to use for funding a tester account. + testKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") + + // testAddr is the Ethereum address of the tester account. + testAddr = crypto.PubkeyToAddress(testKey.PublicKey) + + testValidatorKey, _ = crypto.HexToECDSA("28c3cd61b687fdd03488e167a5d84f50269df2a4c29a2cfb1390903aa775c5d0") + testValidatorAddr = crypto.PubkeyToAddress(testValidatorKey.PublicKey) + + testBuilderKeyHex = "0bfbbbc68fefd990e61ba645efb84e0a62e94d5fff02c9b1da8eb45fea32b4e0" + testBuilderKey, _ = crypto.HexToECDSA(testBuilderKeyHex) + testBuilderAddr = crypto.PubkeyToAddress(testBuilderKey.PublicKey) + + testBalance = big.NewInt(2e18) + + // This EVM code generates a log when the contract is created. + logCode = common.Hex2Bytes("60606040525b7f24ec1d3ff24c2f6ff210738839dbc339cd45a5294d85c79361016243157aae7b60405180905060405180910390a15b600a8060416000396000f360606040526008565b00") +) + +func TestValidateBuilderSubmissionV1(t *testing.T) { + genesis, preMergeBlocks := generatePreMergeChain(20) + os.Setenv("BUILDER_TX_SIGNING_KEY", "0x28c3cd61b687fdd03488e167a5d84f50269df2a4c29a2cfb1390903aa775c5d0") + n, ethservice := startEthService(t, genesis, preMergeBlocks) + ethservice.Merger().ReachTTD() + defer n.Close() + + api := NewBlockValidationAPI(ethservice, nil, true) + parent := preMergeBlocks[len(preMergeBlocks)-1] + + api.eth.APIBackend.Miner().SetEtherbase(testValidatorAddr) + + statedb, _ := ethservice.BlockChain().StateAt(parent.Root()) + nonce := statedb.GetNonce(testAddr) + + tx1, _ := types.SignTx(types.NewTransaction(nonce, common.Address{0x16}, big.NewInt(10), 21000, big.NewInt(2*params.InitialBaseFee), nil), types.LatestSigner(ethservice.BlockChain().Config()), testKey) + ethservice.TxPool().AddLocal(tx1) + + cc, _ := types.SignTx(types.NewContractCreation(nonce+1, new(big.Int), 1000000, big.NewInt(2*params.InitialBaseFee), logCode), types.LatestSigner(ethservice.BlockChain().Config()), testKey) + ethservice.TxPool().AddLocal(cc) + + baseFee := misc.CalcBaseFee(params.AllEthashProtocolChanges, preMergeBlocks[len(preMergeBlocks)-1].Header()) + tx2, _ := types.SignTx(types.NewTransaction(nonce+2, testAddr, big.NewInt(10), 21000, baseFee, nil), types.LatestSigner(ethservice.BlockChain().Config()), testKey) + ethservice.TxPool().AddLocal(tx2) + + execData, err := assembleBlock(api, parent.Hash(), &engine.PayloadAttributes{ + Timestamp: parent.Time() + 5, + SuggestedFeeRecipient: testValidatorAddr, + }) + require.EqualValues(t, len(execData.Transactions), 4) + require.NoError(t, err) + + payload, err := ExecutableDataToExecutionPayload(execData) + require.NoError(t, err) + + proposerAddr := bellatrix.ExecutionAddress{} + copy(proposerAddr[:], testValidatorAddr[:]) + + blockRequest := &BuilderBlockValidationRequest{ + SubmitBlockRequest: bellatrixapi.SubmitBlockRequest{ + Signature: phase0.BLSSignature{}, + Message: &apiv1.BidTrace{ + ParentHash: phase0.Hash32(execData.ParentHash), + BlockHash: phase0.Hash32(execData.BlockHash), + ProposerFeeRecipient: proposerAddr, + GasLimit: execData.GasLimit, + GasUsed: execData.GasUsed, + }, + ExecutionPayload: payload, + }, + RegisteredGasLimit: execData.GasLimit, + } + + blockRequest.Message.Value = uint256.NewInt(190526394825529) + require.ErrorContains(t, api.ValidateBuilderSubmissionV1(blockRequest), "inaccurate payment") + blockRequest.Message.Value = uint256.NewInt(149830884438530) + require.NoError(t, api.ValidateBuilderSubmissionV1(blockRequest)) + + blockRequest.Message.GasLimit += 1 + blockRequest.ExecutionPayload.GasLimit += 1 + updatePayloadHash(t, blockRequest) + + require.ErrorContains(t, api.ValidateBuilderSubmissionV1(blockRequest), "incorrect gas limit set") + + blockRequest.Message.GasLimit -= 1 + blockRequest.ExecutionPayload.GasLimit -= 1 + updatePayloadHash(t, blockRequest) + + // TODO: test with contract calling blacklisted address + // Test tx from blacklisted address + api.accessVerifier = &AccessVerifier{ + blacklistedAddresses: map[common.Address]struct{}{ + testAddr: {}, + }, + } + require.ErrorContains(t, api.ValidateBuilderSubmissionV1(blockRequest), "transaction from blacklisted address 0x71562b71999873DB5b286dF957af199Ec94617F7") + + // Test tx to blacklisted address + api.accessVerifier = &AccessVerifier{ + blacklistedAddresses: map[common.Address]struct{}{ + {0x16}: {}, + }, + } + require.ErrorContains(t, api.ValidateBuilderSubmissionV1(blockRequest), "transaction to blacklisted address 0x1600000000000000000000000000000000000000") + + api.accessVerifier = nil + + blockRequest.Message.GasUsed = 10 + require.ErrorContains(t, api.ValidateBuilderSubmissionV1(blockRequest), "incorrect GasUsed 10, expected 119990") + blockRequest.Message.GasUsed = execData.GasUsed + + newTestKey, _ := crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f290") + invalidTx, err := types.SignTx(types.NewTransaction(0, common.Address{}, new(big.Int).Mul(big.NewInt(2e18), big.NewInt(10)), 19000, big.NewInt(2*params.InitialBaseFee), nil), types.LatestSigner(ethservice.BlockChain().Config()), newTestKey) + require.NoError(t, err) + + txData, err := invalidTx.MarshalBinary() + require.NoError(t, err) + execData.Transactions = append(execData.Transactions, txData) + + invalidPayload, err := ExecutableDataToExecutionPayload(execData) + require.NoError(t, err) + invalidPayload.GasUsed = execData.GasUsed + copy(invalidPayload.ReceiptsRoot[:], hexutil.MustDecode("0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421")[:32]) + blockRequest.ExecutionPayload = invalidPayload + updatePayloadHash(t, blockRequest) + require.ErrorContains(t, api.ValidateBuilderSubmissionV1(blockRequest), "could not apply tx 4", "insufficient funds for gas * price + value") +} + +func TestValidateBuilderSubmissionV2(t *testing.T) { + genesis, preMergeBlocks := generatePreMergeChain(20) + os.Setenv("BUILDER_TX_SIGNING_KEY", testBuilderKeyHex) + time := preMergeBlocks[len(preMergeBlocks)-1].Time() + 5 + genesis.Config.ShanghaiTime = &time + n, ethservice := startEthService(t, genesis, preMergeBlocks) + ethservice.Merger().ReachTTD() + defer n.Close() + + api := NewBlockValidationAPI(ethservice, nil, true) + parent := preMergeBlocks[len(preMergeBlocks)-1] + + api.eth.APIBackend.Miner().SetEtherbase(testBuilderAddr) + + statedb, _ := ethservice.BlockChain().StateAt(parent.Root()) + nonce := statedb.GetNonce(testAddr) + + tx1, _ := types.SignTx(types.NewTransaction(nonce, common.Address{0x16}, big.NewInt(10), 21000, big.NewInt(2*params.InitialBaseFee), nil), types.LatestSigner(ethservice.BlockChain().Config()), testKey) + ethservice.TxPool().AddLocal(tx1) + + cc, _ := types.SignTx(types.NewContractCreation(nonce+1, new(big.Int), 1000000, big.NewInt(2*params.InitialBaseFee), logCode), types.LatestSigner(ethservice.BlockChain().Config()), testKey) + ethservice.TxPool().AddLocal(cc) + + baseFee := misc.CalcBaseFee(params.AllEthashProtocolChanges, preMergeBlocks[len(preMergeBlocks)-1].Header()) + tx2, _ := types.SignTx(types.NewTransaction(nonce+2, testAddr, big.NewInt(10), 21000, baseFee, nil), types.LatestSigner(ethservice.BlockChain().Config()), testKey) + ethservice.TxPool().AddLocal(tx2) + + withdrawals := []*types.Withdrawal{ + { + Index: 0, + Validator: 1, + Amount: 100, + Address: testAddr, + }, + { + Index: 1, + Validator: 1, + Amount: 100, + Address: testAddr, + }, + } + withdrawalsRoot := types.DeriveSha(types.Withdrawals(withdrawals), trie.NewStackTrie(nil)) + + execData, err := assembleBlock(api, parent.Hash(), &engine.PayloadAttributes{ + Timestamp: parent.Time() + 5, + Withdrawals: withdrawals, + SuggestedFeeRecipient: testValidatorAddr, + }) + require.NoError(t, err) + require.EqualValues(t, len(execData.Withdrawals), 2) + require.EqualValues(t, len(execData.Transactions), 4) + + payload, err := ExecutableDataToExecutionPayloadV2(execData) + require.NoError(t, err) + + proposerAddr := bellatrix.ExecutionAddress{} + copy(proposerAddr[:], testValidatorAddr.Bytes()) + + blockRequest := &BuilderBlockValidationRequestV2{ + SubmitBlockRequest: capellaapi.SubmitBlockRequest{ + Signature: phase0.BLSSignature{}, + Message: &apiv1.BidTrace{ + ParentHash: phase0.Hash32(execData.ParentHash), + BlockHash: phase0.Hash32(execData.BlockHash), + ProposerFeeRecipient: proposerAddr, + GasLimit: execData.GasLimit, + GasUsed: execData.GasUsed, + // This value is actual profit + 1, validation should fail + Value: uint256.NewInt(149842511727213), + }, + ExecutionPayload: payload, + }, + RegisteredGasLimit: execData.GasLimit, + WithdrawalsRoot: withdrawalsRoot, + } + + require.ErrorContains(t, api.ValidateBuilderSubmissionV2(blockRequest), "inaccurate payment") + blockRequest.Message.Value = uint256.NewInt(149842511727212) + require.NoError(t, api.ValidateBuilderSubmissionV2(blockRequest)) + + blockRequest.Message.GasLimit += 1 + blockRequest.ExecutionPayload.GasLimit += 1 + updatePayloadHashV2(t, blockRequest) + + require.ErrorContains(t, api.ValidateBuilderSubmissionV2(blockRequest), "incorrect gas limit set") + + blockRequest.Message.GasLimit -= 1 + blockRequest.ExecutionPayload.GasLimit -= 1 + updatePayloadHashV2(t, blockRequest) + + // TODO: test with contract calling blacklisted address + // Test tx from blacklisted address + api.accessVerifier = &AccessVerifier{ + blacklistedAddresses: map[common.Address]struct{}{ + testAddr: {}, + }, + } + require.ErrorContains(t, api.ValidateBuilderSubmissionV2(blockRequest), "transaction from blacklisted address 0x71562b71999873DB5b286dF957af199Ec94617F7") + + // Test tx to blacklisted address + api.accessVerifier = &AccessVerifier{ + blacklistedAddresses: map[common.Address]struct{}{ + {0x16}: {}, + }, + } + require.ErrorContains(t, api.ValidateBuilderSubmissionV2(blockRequest), "transaction to blacklisted address 0x1600000000000000000000000000000000000000") + + api.accessVerifier = nil + + blockRequest.Message.GasUsed = 10 + require.ErrorContains(t, api.ValidateBuilderSubmissionV2(blockRequest), "incorrect GasUsed 10, expected 119996") + blockRequest.Message.GasUsed = execData.GasUsed + + newTestKey, _ := crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f290") + invalidTx, err := types.SignTx(types.NewTransaction(0, common.Address{}, new(big.Int).Mul(big.NewInt(2e18), big.NewInt(10)), 19000, big.NewInt(2*params.InitialBaseFee), nil), types.LatestSigner(ethservice.BlockChain().Config()), newTestKey) + require.NoError(t, err) + + txData, err := invalidTx.MarshalBinary() + require.NoError(t, err) + execData.Transactions = append(execData.Transactions, txData) + + invalidPayload, err := ExecutableDataToExecutionPayloadV2(execData) + require.NoError(t, err) + invalidPayload.GasUsed = execData.GasUsed + copy(invalidPayload.ReceiptsRoot[:], hexutil.MustDecode("0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421")[:32]) + blockRequest.ExecutionPayload = invalidPayload + updatePayloadHashV2(t, blockRequest) + require.ErrorContains(t, api.ValidateBuilderSubmissionV2(blockRequest), "could not apply tx 4", "insufficient funds for gas * price + value") +} + +func updatePayloadHash(t *testing.T, blockRequest *BuilderBlockValidationRequest) { + updatedBlock, err := engine.ExecutionPayloadToBlock(blockRequest.ExecutionPayload) + require.NoError(t, err) + copy(blockRequest.Message.BlockHash[:], updatedBlock.Hash().Bytes()[:32]) +} + +func updatePayloadHashV2(t *testing.T, blockRequest *BuilderBlockValidationRequestV2) { + updatedBlock, err := engine.ExecutionPayloadV2ToBlock(blockRequest.ExecutionPayload) + require.NoError(t, err) + copy(blockRequest.Message.BlockHash[:], updatedBlock.Hash().Bytes()[:32]) +} + +func generatePreMergeChain(n int) (*core.Genesis, []*types.Block) { + db := rawdb.NewMemoryDatabase() + config := params.AllEthashProtocolChanges + genesis := &core.Genesis{ + Config: config, + Alloc: core.GenesisAlloc{testAddr: {Balance: testBalance}, testValidatorAddr: {Balance: testBalance}, testBuilderAddr: {Balance: testBalance}}, + ExtraData: []byte("test genesis"), + Timestamp: 9000, + BaseFee: big.NewInt(params.InitialBaseFee), + Difficulty: big.NewInt(0), + } + testNonce := uint64(0) + generate := func(i int, g *core.BlockGen) { + g.OffsetTime(5) + g.SetExtra([]byte("test")) + tx, _ := types.SignTx(types.NewTransaction(testNonce, common.HexToAddress("0x9a9070028361F7AAbeB3f2F2Dc07F82C4a98A02a"), big.NewInt(1), params.TxGas, big.NewInt(params.InitialBaseFee*2), nil), types.LatestSigner(config), testKey) + g.AddTx(tx) + testNonce++ + } + gblock := genesis.MustCommit(db) + engine := ethash.NewFaker() + blocks, _ := core.GenerateChain(config, gblock, engine, db, n, generate) + totalDifficulty := big.NewInt(0) + for _, b := range blocks { + totalDifficulty.Add(totalDifficulty, b.Difficulty()) + } + config.TerminalTotalDifficulty = totalDifficulty + return genesis, blocks +} + +// startEthService creates a full node instance for testing. +func startEthService(t *testing.T, genesis *core.Genesis, blocks []*types.Block) (*node.Node, *eth.Ethereum) { + t.Helper() + + n, err := node.New(&node.Config{ + P2P: p2p.Config{ + ListenAddr: "0.0.0.0:0", + NoDiscovery: true, + MaxPeers: 25, + }}) + if err != nil { + t.Fatal("can't create node:", err) + } + + ethcfg := ðconfig.Config{Genesis: genesis, Ethash: ethash.Config{PowMode: ethash.ModeFake}, SyncMode: downloader.SnapSync, TrieTimeout: time.Minute, TrieDirtyCache: 256, TrieCleanCache: 256} + ethservice, err := eth.New(n, ethcfg) + if err != nil { + t.Fatal("can't create eth service:", err) + } + if err := n.Start(); err != nil { + t.Fatal("can't start node:", err) + } + if _, err := ethservice.BlockChain().InsertChain(blocks); err != nil { + n.Close() + t.Fatal("can't import test blocks:", err) + } + time.Sleep(500 * time.Millisecond) // give txpool enough time to consume head event + + ethservice.SetEtherbase(testAddr) + ethservice.SetSynced() + return n, ethservice +} + +func assembleBlock(api *BlockValidationAPI, parentHash common.Hash, params *engine.PayloadAttributes) (*engine.ExecutableData, error) { + args := &miner.BuildPayloadArgs{ + Parent: parentHash, + Timestamp: params.Timestamp, + FeeRecipient: params.SuggestedFeeRecipient, + GasLimit: params.GasLimit, + Random: params.Random, + Withdrawals: params.Withdrawals, + } + + payload, err := api.eth.Miner().BuildPayload(args) + if err != nil { + return nil, err + } + + if payload := payload.ResolveFull(); payload != nil { + return payload.ExecutionPayload, nil + } + + return nil, errors.New("payload did not resolve") +} + +func TestBlacklistLoad(t *testing.T) { + file, err := os.CreateTemp(".", "blacklist") + require.NoError(t, err) + defer os.Remove(file.Name()) + + av, err := NewAccessVerifierFromFile(file.Name()) + require.Error(t, err) + require.Nil(t, av) + + ba := BlacklistedAddresses{common.Address{0x13}, common.Address{0x14}} + bytes, err := json.MarshalIndent(ba, "", " ") + require.NoError(t, err) + err = os.WriteFile(file.Name(), bytes, 0644) + require.NoError(t, err) + + av, err = NewAccessVerifierFromFile(file.Name()) + require.NoError(t, err) + require.NotNil(t, av) + require.EqualValues(t, av.blacklistedAddresses, map[common.Address]struct{}{ + {0x13}: {}, + {0x14}: {}, + }) + + require.NoError(t, av.verifyTraces(logger.NewAccessListTracer(nil, common.Address{}, common.Address{}, nil))) + + acl := types.AccessList{ + types.AccessTuple{ + Address: common.Address{0x14}, + }, + } + tracer := logger.NewAccessListTracer(acl, common.Address{}, common.Address{}, nil) + require.ErrorContains(t, av.verifyTraces(tracer), "blacklisted address 0x1400000000000000000000000000000000000000 in execution trace") + + acl = types.AccessList{ + types.AccessTuple{ + Address: common.Address{0x15}, + }, + } + tracer = logger.NewAccessListTracer(acl, common.Address{}, common.Address{}, nil) + require.NoError(t, av.verifyTraces(tracer)) +} + +func ExecutableDataToExecutionPayload(data *engine.ExecutableData) (*bellatrix.ExecutionPayload, error) { + transactionData := make([]bellatrix.Transaction, len(data.Transactions)) + for i, tx := range data.Transactions { + transactionData[i] = bellatrix.Transaction(tx) + } + + baseFeePerGas := new(boostTypes.U256Str) + err := baseFeePerGas.FromBig(data.BaseFeePerGas) + if err != nil { + return nil, err + } + + return &bellatrix.ExecutionPayload{ + ParentHash: [32]byte(data.ParentHash), + FeeRecipient: [20]byte(data.FeeRecipient), + StateRoot: [32]byte(data.StateRoot), + ReceiptsRoot: [32]byte(data.ReceiptsRoot), + LogsBloom: types.BytesToBloom(data.LogsBloom), + PrevRandao: [32]byte(data.Random), + BlockNumber: data.Number, + GasLimit: data.GasLimit, + GasUsed: data.GasUsed, + Timestamp: data.Timestamp, + ExtraData: data.ExtraData, + BaseFeePerGas: *baseFeePerGas, + BlockHash: [32]byte(data.BlockHash), + Transactions: transactionData, + }, nil +} + +func ExecutableDataToExecutionPayloadV2(data *engine.ExecutableData) (*capella.ExecutionPayload, error) { + transactionData := make([]bellatrix.Transaction, len(data.Transactions)) + for i, tx := range data.Transactions { + transactionData[i] = bellatrix.Transaction(tx) + } + + withdrawalData := make([]*capella.Withdrawal, len(data.Withdrawals)) + for i, withdrawal := range data.Withdrawals { + withdrawalData[i] = &capella.Withdrawal{ + Index: capella.WithdrawalIndex(withdrawal.Index), + ValidatorIndex: phase0.ValidatorIndex(withdrawal.Validator), + Address: bellatrix.ExecutionAddress(withdrawal.Address), + Amount: phase0.Gwei(withdrawal.Amount), + } + } + + baseFeePerGas := new(boostTypes.U256Str) + err := baseFeePerGas.FromBig(data.BaseFeePerGas) + if err != nil { + return nil, err + } + + return &capella.ExecutionPayload{ + ParentHash: [32]byte(data.ParentHash), + FeeRecipient: [20]byte(data.FeeRecipient), + StateRoot: [32]byte(data.StateRoot), + ReceiptsRoot: [32]byte(data.ReceiptsRoot), + LogsBloom: types.BytesToBloom(data.LogsBloom), + PrevRandao: [32]byte(data.Random), + BlockNumber: data.Number, + GasLimit: data.GasLimit, + GasUsed: data.GasUsed, + Timestamp: data.Timestamp, + ExtraData: data.ExtraData, + BaseFeePerGas: *baseFeePerGas, + BlockHash: [32]byte(data.BlockHash), + Transactions: transactionData, + Withdrawals: withdrawalData, + }, nil +} + +func WithdrawalToBlockRequestWithdrawal(withdrawals types.Withdrawals) []*capella.Withdrawal { + withdrawalsData := make([]*capella.Withdrawal, len(withdrawals)) + for i, withdrawal := range withdrawals { + withdrawalsData[i] = &capella.Withdrawal{ + Index: capella.WithdrawalIndex(i), + ValidatorIndex: phase0.ValidatorIndex(withdrawal.Validator), + Address: bellatrix.ExecutionAddress(withdrawal.Address), + Amount: phase0.Gwei(withdrawal.Amount), + } + } + return withdrawalsData +} + +type buildBlockArgs struct { + parentHash common.Hash + parentRoot common.Hash + feeRecipient common.Address + txs types.Transactions + random common.Hash + number uint64 + gasLimit uint64 + timestamp uint64 + extraData []byte + baseFeePerGas *big.Int + withdrawals types.Withdrawals +} + +func buildBlock(args buildBlockArgs, chain *core.BlockChain) (*engine.ExecutableData, error) { + header := &types.Header{ + ParentHash: args.parentHash, + Coinbase: args.feeRecipient, + Number: big.NewInt(int64(args.number)), + GasLimit: args.gasLimit, + Time: args.timestamp, + Extra: args.extraData, + BaseFee: args.baseFeePerGas, + MixDigest: args.random, + } + + err := chain.Engine().Prepare(chain, header) + if err != nil { + return nil, err + } + + statedb, err := chain.StateAt(args.parentRoot) + if err != nil { + return nil, err + } + + receipts := make([]*types.Receipt, 0, len(args.txs)) + gasPool := core.GasPool(header.GasLimit) + vmConfig := vm.Config{} + for i, tx := range args.txs { + statedb.SetTxContext(tx.Hash(), i) + receipt, err := core.ApplyTransaction(chain.Config(), chain, &args.feeRecipient, &gasPool, statedb, header, tx, &header.GasUsed, vmConfig, nil) + if err != nil { + return nil, err + } + receipts = append(receipts, receipt) + } + + block, err := chain.Engine().FinalizeAndAssemble(chain, header, statedb, args.txs, nil, receipts, args.withdrawals) + if err != nil { + return nil, err + } + + execData := engine.BlockToExecutableData(block, common.Big0) + + return execData.ExecutionPayload, nil +} + +func executableDataToBlockValidationRequest(execData *engine.ExecutableData, proposer common.Address, value *big.Int, withdrawalsRoot common.Hash) (*BuilderBlockValidationRequestV2, error) { + payload, err := ExecutableDataToExecutionPayloadV2(execData) + if err != nil { + return nil, err + } + + proposerAddr := bellatrix.ExecutionAddress{} + copy(proposerAddr[:], proposer.Bytes()) + + value256, overflow := uint256.FromBig(value) + if overflow { + return nil, errors.New("could not convert value to uint256") + } + blockRequest := &BuilderBlockValidationRequestV2{ + SubmitBlockRequest: capellaapi.SubmitBlockRequest{ + Signature: phase0.BLSSignature{}, + Message: &apiv1.BidTrace{ + ParentHash: phase0.Hash32(execData.ParentHash), + BlockHash: phase0.Hash32(execData.BlockHash), + ProposerFeeRecipient: proposerAddr, + GasLimit: execData.GasLimit, + GasUsed: execData.GasUsed, + Value: value256, + }, + ExecutionPayload: payload, + }, + RegisteredGasLimit: execData.GasLimit, + WithdrawalsRoot: withdrawalsRoot, + } + return blockRequest, nil +} + +// This tests payment when the proposer fee recipient is the same as the coinbase +func TestValidateBuilderSubmissionV2_CoinbasePaymentDefault(t *testing.T) { + genesis, preMergeBlocks := generatePreMergeChain(20) + lastBlock := preMergeBlocks[len(preMergeBlocks)-1] + time := lastBlock.Time() + 5 + genesis.Config.ShanghaiTime = &time + n, ethservice := startEthService(t, genesis, preMergeBlocks) + ethservice.Merger().ReachTTD() + defer n.Close() + + api := NewBlockValidationAPI(ethservice, nil, true) + + baseFee := misc.CalcBaseFee(ethservice.BlockChain().Config(), lastBlock.Header()) + txs := make(types.Transactions, 0) + + statedb, _ := ethservice.BlockChain().StateAt(lastBlock.Root()) + nonce := statedb.GetNonce(testAddr) + signer := types.LatestSigner(ethservice.BlockChain().Config()) + + expectedProfit := uint64(0) + + tx1, _ := types.SignTx(types.NewTransaction(nonce, common.Address{0x16}, big.NewInt(10), 21000, big.NewInt(2*baseFee.Int64()), nil), signer, testKey) + txs = append(txs, tx1) + expectedProfit += 21000 * baseFee.Uint64() + + // this tx will use 56996 gas + tx2, _ := types.SignTx(types.NewContractCreation(nonce+1, new(big.Int), 1000000, big.NewInt(2*baseFee.Int64()), logCode), signer, testKey) + txs = append(txs, tx2) + expectedProfit += 56996 * baseFee.Uint64() + + tx3, _ := types.SignTx(types.NewTransaction(nonce+2, testAddr, big.NewInt(10), 21000, baseFee, nil), signer, testKey) + txs = append(txs, tx3) + + // this transaction sends 7 wei to the proposer fee recipient, this should count as a profit + tx4, _ := types.SignTx(types.NewTransaction(nonce+3, testValidatorAddr, big.NewInt(7), 21000, baseFee, nil), signer, testKey) + txs = append(txs, tx4) + expectedProfit += 7 + + // transactions from the proposer fee recipient + + // this transaction sends 3 wei from the proposer fee recipient to the proposer fee recipient and pays tip of baseFee + // this should not count as a profit (because balance does not increase) + // Base fee is burned from the balance so it should decrease decreasing the profit. + tx5, _ := types.SignTx(types.NewTransaction(0, testValidatorAddr, big.NewInt(3), 21000, big.NewInt(2*baseFee.Int64()), nil), signer, testValidatorKey) + txs = append(txs, tx5) + expectedProfit -= 21000 * baseFee.Uint64() + + // this tx sends 11 wei from the proposer fee recipient to some other address and burns 21000*baseFee + // this should count as negative profit + tx6, _ := types.SignTx(types.NewTransaction(1, testAddr, big.NewInt(11), 21000, baseFee, nil), signer, testValidatorKey) + txs = append(txs, tx6) + expectedProfit -= 11 + 21000*baseFee.Uint64() + + withdrawals := []*types.Withdrawal{ + { + Index: 0, + Validator: 1, + Amount: 100, + Address: testAddr, + }, + { + Index: 1, + Validator: 1, + Amount: 100, + Address: testAddr, + }, + } + withdrawalsRoot := types.DeriveSha(types.Withdrawals(withdrawals), trie.NewStackTrie(nil)) + + buildBlockArgs := buildBlockArgs{ + parentHash: lastBlock.Hash(), + parentRoot: lastBlock.Root(), + feeRecipient: testValidatorAddr, + txs: txs, + random: common.Hash{}, + number: lastBlock.NumberU64() + 1, + gasLimit: lastBlock.GasLimit(), + timestamp: lastBlock.Time() + 5, + extraData: nil, + baseFeePerGas: baseFee, + withdrawals: withdrawals, + } + + execData, err := buildBlock(buildBlockArgs, ethservice.BlockChain()) + require.NoError(t, err) + + value := big.NewInt(int64(expectedProfit)) + + req, err := executableDataToBlockValidationRequest(execData, testValidatorAddr, value, withdrawalsRoot) + require.NoError(t, err) + require.NoError(t, api.ValidateBuilderSubmissionV2(req)) + + // try to claim less profit than expected, should work + value.SetUint64(expectedProfit - 1) + + req, err = executableDataToBlockValidationRequest(execData, testValidatorAddr, value, withdrawalsRoot) + require.NoError(t, err) + require.NoError(t, api.ValidateBuilderSubmissionV2(req)) + + // try to claim more profit than expected, should fail + value.SetUint64(expectedProfit + 1) + + req, err = executableDataToBlockValidationRequest(execData, testValidatorAddr, value, withdrawalsRoot) + require.NoError(t, err) + require.ErrorContains(t, api.ValidateBuilderSubmissionV2(req), "payment") +} + +func TestValidateBuilderSubmissionV2_Blocklist(t *testing.T) { + genesis, preMergeBlocks := generatePreMergeChain(20) + lastBlock := preMergeBlocks[len(preMergeBlocks)-1] + time := lastBlock.Time() + 5 + genesis.Config.ShanghaiTime = &time + n, ethservice := startEthService(t, genesis, preMergeBlocks) + ethservice.Merger().ReachTTD() + defer n.Close() + + accessVerifier := &AccessVerifier{ + blacklistedAddresses: map[common.Address]struct{}{ + testAddr: {}, + }, + } + + apiWithBlock := NewBlockValidationAPI(ethservice, accessVerifier, true) + apiNoBlock := NewBlockValidationAPI(ethservice, nil, true) + + baseFee := misc.CalcBaseFee(ethservice.BlockChain().Config(), lastBlock.Header()) + blockedTxs := make(types.Transactions, 0) + + statedb, _ := ethservice.BlockChain().StateAt(lastBlock.Root()) + + signer := types.LatestSigner(ethservice.BlockChain().Config()) + + nonce := statedb.GetNonce(testAddr) + tx, _ := types.SignTx(types.NewTransaction(nonce, common.Address{0x16}, big.NewInt(10), 21000, baseFee, nil), signer, testKey) + blockedTxs = append(blockedTxs, tx) + + nonce = statedb.GetNonce(testBuilderAddr) + tx, _ = types.SignTx(types.NewTransaction(nonce, testAddr, big.NewInt(10), 21000, baseFee, nil), signer, testBuilderKey) + blockedTxs = append(blockedTxs, tx) + + withdrawalsRoot := types.DeriveSha(types.Withdrawals(nil), trie.NewStackTrie(nil)) + + for i, tx := range blockedTxs { + t.Run(fmt.Sprintf("tx %d", i), func(t *testing.T) { + buildBlockArgs := buildBlockArgs{ + parentHash: lastBlock.Hash(), + parentRoot: lastBlock.Root(), + feeRecipient: testValidatorAddr, + txs: types.Transactions{tx}, + random: common.Hash{}, + number: lastBlock.NumberU64() + 1, + gasLimit: lastBlock.GasLimit(), + timestamp: lastBlock.Time() + 5, + extraData: nil, + baseFeePerGas: baseFee, + withdrawals: nil, + } + + execData, err := buildBlock(buildBlockArgs, ethservice.BlockChain()) + require.NoError(t, err) + + req, err := executableDataToBlockValidationRequest(execData, testValidatorAddr, common.Big0, withdrawalsRoot) + require.NoError(t, err) + + require.NoError(t, apiNoBlock.ValidateBuilderSubmissionV2(req)) + require.ErrorContains(t, apiWithBlock.ValidateBuilderSubmissionV2(req), "blacklisted") + }) + } +} diff --git a/flashbotsextra/cmd/bundle_fetcher.go b/flashbotsextra/cmd/bundle_fetcher.go new file mode 100644 index 0000000000..ff6e78be93 --- /dev/null +++ b/flashbotsextra/cmd/bundle_fetcher.go @@ -0,0 +1,37 @@ +package main + +import ( + "os" + "time" + + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/flashbotsextra" + "github.com/ethereum/go-ethereum/log" +) + +func main() { + // Test bundle fetcher + log.Root().SetHandler(log.LvlFilterHandler(log.LvlInfo, log.StreamHandler(os.Stderr, log.TerminalFormat(true)))) + mevBundleCh := make(chan []types.MevBundle) + blockNumCh := make(chan int64) + db, err := flashbotsextra.NewDatabaseService("postgres://postgres:postgres@localhost:5432/test?sslmode=disable") + if err != nil { + panic(err) + } + bundleFetcher := flashbotsextra.NewBundleFetcher(nil, db, blockNumCh, mevBundleCh, false) + + go bundleFetcher.Run() + log.Info("waiting for mev bundles") + go func() { + blockNum := []int64{15232009, 15232008, 15232010} + for _, num := range blockNum { + <-time.After(time.Second) + blockNumCh <- num + } + }() + for bundles := range mevBundleCh { + for _, bundle := range bundles { + log.Info("bundle info", "blockNum", bundle.BlockNumber, "txsLength", len(bundle.Txs)) + } + } +} diff --git a/flashbotsextra/database.go b/flashbotsextra/database.go new file mode 100644 index 0000000000..80e8170607 --- /dev/null +++ b/flashbotsextra/database.go @@ -0,0 +1,370 @@ +package flashbotsextra + +import ( + "context" + "database/sql" + "math/big" + "time" + + apiv1 "github.com/attestantio/go-builder-client/api/v1" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/log" + "github.com/google/uuid" + "github.com/jmoiron/sqlx" + _ "github.com/lib/pq" +) + +const ( + highPrioLimitSize = 500 + lowPrioLimitSize = 100 +) + +type IDatabaseService interface { + ConsumeBuiltBlock(block *types.Block, blockValue *big.Int, OrdersClosedAt time.Time, sealedAt time.Time, + commitedBundles []types.SimulatedBundle, allBundles []types.SimulatedBundle, + usedSbundles []types.UsedSBundle, + bidTrace *apiv1.BidTrace) + GetPriorityBundles(ctx context.Context, blockNum int64, isHighPrio bool) ([]DbBundle, error) + GetLatestUuidBundles(ctx context.Context, blockNum int64) ([]types.LatestUuidBundle, error) +} + +type NilDbService struct{} + +func (NilDbService) ConsumeBuiltBlock(block *types.Block, _ *big.Int, _ time.Time, _ time.Time, _ []types.SimulatedBundle, _ []types.SimulatedBundle, _ []types.UsedSBundle, _ *apiv1.BidTrace) { +} + +func (NilDbService) GetPriorityBundles(ctx context.Context, blockNum int64, isHighPrio bool) ([]DbBundle, error) { + return []DbBundle{}, nil +} + +func (NilDbService) GetLatestUuidBundles(ctx context.Context, blockNum int64) ([]types.LatestUuidBundle, error) { + return []types.LatestUuidBundle{}, nil +} + +type DatabaseService struct { + db *sqlx.DB + + insertBuiltBlockStmt *sqlx.NamedStmt + insertMissingBundleStmt *sqlx.NamedStmt + fetchPrioBundlesStmt *sqlx.NamedStmt + fetchGetLatestUuidBundlesStmt *sqlx.NamedStmt +} + +func NewDatabaseService(postgresDSN string) (*DatabaseService, error) { + db, err := sqlx.Connect("postgres", postgresDSN) + if err != nil { + return nil, err + } + + insertBuiltBlockStmt, err := db.PrepareNamed("insert into built_blocks (block_number, profit, slot, hash, gas_limit, gas_used, base_fee, parent_hash, proposer_pubkey, proposer_fee_recipient, builder_pubkey, timestamp, timestamp_datetime, orders_closed_at, sealed_at) values (:block_number, :profit, :slot, :hash, :gas_limit, :gas_used, :base_fee, :parent_hash, :proposer_pubkey, :proposer_fee_recipient, :builder_pubkey, :timestamp, to_timestamp(:timestamp), :orders_closed_at, :sealed_at) returning block_id") + if err != nil { + return nil, err + } + + insertMissingBundleStmt, err := db.PrepareNamed("insert into bundles (bundle_hash, param_signed_txs, param_block_number, param_timestamp, received_timestamp, param_reverting_tx_hashes, coinbase_diff, total_gas_used, state_block_number, gas_fees, eth_sent_to_coinbase, bundle_uuid) values (:bundle_hash, :param_signed_txs, :param_block_number, :param_timestamp, :received_timestamp, :param_reverting_tx_hashes, :coinbase_diff, :total_gas_used, :state_block_number, :gas_fees, :eth_sent_to_coinbase, :bundle_uuid) on conflict do nothing returning id") + if err != nil { + return nil, err + } + + fetchPrioBundlesStmt, err := db.PrepareNamed("select bundle_hash, param_signed_txs, param_block_number, param_timestamp, received_timestamp, param_reverting_tx_hashes, coinbase_diff, total_gas_used, state_block_number, gas_fees, eth_sent_to_coinbase, bundle_uuid from bundles where is_high_prio = :is_high_prio and coinbase_diff*1e18/total_gas_used > 1000000000 and param_block_number = :param_block_number order by coinbase_diff/total_gas_used DESC limit :limit") + if err != nil { + return nil, err + } + + fetchGetLatestUuidBundlesStmt, err := db.PrepareNamed("select replacement_uuid, signing_address, bundle_hash, bundle_uuid from latest_uuid_bundle where target_block_number = :target_block_number") + if err != nil { + return nil, err + } + + return &DatabaseService{ + db: db, + insertBuiltBlockStmt: insertBuiltBlockStmt, + insertMissingBundleStmt: insertMissingBundleStmt, + fetchPrioBundlesStmt: fetchPrioBundlesStmt, + fetchGetLatestUuidBundlesStmt: fetchGetLatestUuidBundlesStmt, + }, nil +} + +func Min(l int, r int) int { + if l < r { + return l + } + return r +} + +func (ds *DatabaseService) getBundleIds(ctx context.Context, blockNumber uint64, bundles []uuidBundle) (map[uuid.UUID]uint64, error) { + if len(bundles) == 0 { + return nil, nil + } + + bundleIdsMap := make(map[uuid.UUID]uint64, len(bundles)) + + // Batch by 500 + requestsToMake := [][]string{make([]string, 0, Min(500, len(bundles)))} + cRequestInd := 0 + for i, bundle := range bundles { + if i != 0 && i%500 == 0 { + cRequestInd += 1 + requestsToMake = append(requestsToMake, make([]string, 0, Min(500, len(bundles)-i))) + } + requestsToMake[cRequestInd] = append(requestsToMake[cRequestInd], bundle.SimulatedBundle.OriginalBundle.Hash.String()) + } + + for _, request := range requestsToMake { + query, args, err := sqlx.In("select id, bundle_hash, bundle_uuid from bundles where param_block_number = ? and bundle_hash in (?)", blockNumber, request) + if err != nil { + return nil, err + } + query = ds.db.Rebind(query) + + queryRes := []struct { + Id uint64 `db:"id"` + BundleHash string `db:"bundle_hash"` + BundleUUID uuid.UUID `db:"bundle_uuid"` + }{} + + err = ds.db.SelectContext(ctx, &queryRes, query, args...) + if err != nil { + return nil, err + } + RowLoop: + for _, row := range queryRes { + for _, b := range bundles { + // if UUID agree it's same exact bundle we stop searching + if b.UUID == row.BundleUUID { + bundleIdsMap[b.UUID] = row.Id + continue RowLoop + } + // we can have multiple bundles with same hash eventually, so we fall back on getting row with same hash + if b.SimulatedBundle.OriginalBundle.Hash.String() == row.BundleHash { + bundleIdsMap[b.UUID] = row.Id + } + } + } + } + + return bundleIdsMap, nil +} + +// TODO: cache locally for current block! +func (ds *DatabaseService) getBundleIdsAndInsertMissingBundles(ctx context.Context, blockNumber uint64, bundles []uuidBundle) (map[uuid.UUID]uint64, error) { + bundleIdsMap, err := ds.getBundleIds(ctx, blockNumber, bundles) + if err != nil { + return nil, err + } + + toRetry := make([]uuidBundle, 0) + for _, bundle := range bundles { + if _, found := bundleIdsMap[bundle.UUID]; found { + continue + } + + var bundleId uint64 + missingBundleData := SimulatedBundleToDbBundle(&bundle.SimulatedBundle) // nolint: gosec + err = ds.insertMissingBundleStmt.GetContext(ctx, &bundleId, missingBundleData) // not using the tx as it relies on the unique constraint! + if err == nil { + bundleIdsMap[bundle.UUID] = bundleId + } else if err == sql.ErrNoRows /* conflict, someone else inserted the bundle before we could */ { + toRetry = append(toRetry, bundle) + } else { + log.Error("could not insert missing bundle", "err", err) + } + } + + retriedBundleIds, err := ds.getBundleIds(ctx, blockNumber, toRetry) + if err != nil { + return nil, err + } + + for hash, id := range retriedBundleIds { + bundleIdsMap[hash] = id + } + + return bundleIdsMap, nil +} + +func (ds *DatabaseService) insertBuildBlock(tx *sqlx.Tx, ctx context.Context, block *types.Block, blockValue *big.Int, bidTrace *apiv1.BidTrace, ordersClosedAt time.Time, sealedAt time.Time) (uint64, error) { + blockData := BuiltBlock{ + BlockNumber: block.NumberU64(), + Profit: new(big.Rat).SetFrac(blockValue, big.NewInt(1e18)).FloatString(18), + Slot: bidTrace.Slot, + Hash: block.Hash().String(), + GasLimit: block.GasLimit(), + GasUsed: block.GasUsed(), + BaseFee: block.BaseFee().Uint64(), + ParentHash: block.ParentHash().String(), + ProposerPubkey: bidTrace.ProposerPubkey.String(), + ProposerFeeRecipient: bidTrace.ProposerFeeRecipient.String(), + BuilderPubkey: bidTrace.BuilderPubkey.String(), + Timestamp: block.Time(), + OrdersClosedAt: ordersClosedAt.UTC(), + SealedAt: sealedAt.UTC(), + } + + var blockId uint64 + if err := tx.NamedStmtContext(ctx, ds.insertBuiltBlockStmt).GetContext(ctx, &blockId, blockData); err != nil { + log.Error("could not insert built block", "err", err) + return 0, err + } + + return blockId, nil +} + +func (ds *DatabaseService) insertBuildBlockBundleIds(tx *sqlx.Tx, ctx context.Context, blockId uint64, bundleIds []uint64) error { + if len(bundleIds) == 0 { + return nil + } + + toInsert := make([]blockAndBundleId, len(bundleIds)) + for i, bundleId := range bundleIds { + toInsert[i] = blockAndBundleId{blockId, bundleId} + } + + _, err := tx.NamedExecContext(ctx, "insert into built_blocks_bundles (block_id, bundle_id) values (:block_id, :bundle_id)", toInsert) + return err +} + +func (ds *DatabaseService) insertAllBlockBundleIds(tx *sqlx.Tx, ctx context.Context, blockId uint64, bundleIds []uint64) error { + if len(bundleIds) == 0 { + return nil + } + + toInsert := make([]blockAndBundleId, 0, len(bundleIds)) + for _, bundleId := range bundleIds { + toInsert = append(toInsert, blockAndBundleId{blockId, bundleId}) + } + + _, err := tx.NamedExecContext(ctx, "insert into built_blocks_all_bundles (block_id, bundle_id) values (:block_id, :bundle_id)", toInsert) + return err +} + +func (ds *DatabaseService) insertUsedSBundleIds(tx *sqlx.Tx, ctx context.Context, blockId uint64, usedSbundles []types.UsedSBundle) error { + if len(usedSbundles) == 0 { + return nil + } + + toInsert := make([]DbUsedSBundle, len(usedSbundles)) + for i, u := range usedSbundles { + toInsert[i] = DbUsedSBundle{ + BlockId: blockId, + Hash: u.Bundle.Hash().Bytes(), + Inserted: u.Success, + } + } + _, err := tx.NamedExecContext(ctx, insertUsedSbundleQuery, toInsert) + return err +} + +type uuidBundle struct { + SimulatedBundle types.SimulatedBundle + UUID uuid.UUID +} + +func (ds *DatabaseService) ConsumeBuiltBlock(block *types.Block, blockValue *big.Int, ordersClosedAt time.Time, sealedAt time.Time, + commitedBundles []types.SimulatedBundle, allBundles []types.SimulatedBundle, + usedSbundles []types.UsedSBundle, + bidTrace *apiv1.BidTrace) { + var allUUIDBundles = make([]uuidBundle, 0, len(allBundles)) + for _, bundle := range allBundles { + allUUIDBundles = append(allUUIDBundles, uuidBundle{bundle, bundle.OriginalBundle.ComputeUUID()}) + } + + var commitedUUIDBundles = make([]uuidBundle, 0, len(commitedBundles)) + for _, bundle := range commitedBundles { + commitedUUIDBundles = append(commitedUUIDBundles, uuidBundle{bundle, bundle.OriginalBundle.ComputeUUID()}) + } + + ctx, cancel := context.WithTimeout(context.Background(), 12*time.Second) + defer cancel() + + bundleIdsMap, err := ds.getBundleIdsAndInsertMissingBundles(ctx, block.NumberU64(), allUUIDBundles) + if err != nil { + log.Error("could not insert bundles", "err", err) + } + + tx, err := ds.db.Beginx() + if err != nil { + log.Error("could not open DB transaction", "err", err) + return + } + + blockId, err := ds.insertBuildBlock(tx, ctx, block, blockValue, bidTrace, ordersClosedAt, sealedAt) + if err != nil { + tx.Rollback() + log.Error("could not insert built block", "err", err) + return + } + + commitedBundlesIds := make([]uint64, 0, len(commitedBundles)) + for _, bundle := range commitedUUIDBundles { + if id, found := bundleIdsMap[bundle.UUID]; found { + commitedBundlesIds = append(commitedBundlesIds, id) + } + } + + err = ds.insertBuildBlockBundleIds(tx, ctx, blockId, commitedBundlesIds) + if err != nil { + tx.Rollback() + log.Error("could not insert built block bundles", "err", err) + return + } + + var uniqueBundleIDs = make(map[uint64]struct{}) + var allBundleIds []uint64 + // we need to filter out duplicates while we still have unique constraint on bundle_hash+block_number which leads to data discrepancies + for _, v := range bundleIdsMap { + if _, ok := uniqueBundleIDs[v]; ok { + continue + } + uniqueBundleIDs[v] = struct{}{} + allBundleIds = append(allBundleIds, v) + } + err = ds.insertAllBlockBundleIds(tx, ctx, blockId, allBundleIds) + if err != nil { + tx.Rollback() + log.Error("could not insert built block all bundles", "err", err, "block", block.NumberU64(), "commitedBundles", commitedBundlesIds) + return + } + + err = ds.insertUsedSBundleIds(tx, ctx, blockId, usedSbundles) + if err != nil { + tx.Rollback() + log.Error("could not insert used sbundles", "err", err) + return + } + + err = tx.Commit() + if err != nil { + log.Error("could not commit DB trasnaction", "err", err) + } +} +func (ds *DatabaseService) GetPriorityBundles(ctx context.Context, blockNum int64, isHighPrio bool) ([]DbBundle, error) { + var bundles []DbBundle + arg := map[string]interface{}{"param_block_number": uint64(blockNum), "is_high_prio": isHighPrio, "limit": lowPrioLimitSize} + if isHighPrio { + arg["limit"] = highPrioLimitSize + } + if err := ds.fetchPrioBundlesStmt.SelectContext(ctx, &bundles, arg); err != nil { + return nil, err + } + return bundles, nil +} + +func (ds *DatabaseService) GetLatestUuidBundles(ctx context.Context, blockNum int64) ([]types.LatestUuidBundle, error) { + var dstLatestBundles []DbLatestUuidBundle + kwArg := map[string]interface{}{"target_block_number": blockNum} + if err := ds.fetchGetLatestUuidBundlesStmt.SelectContext(ctx, &dstLatestBundles, kwArg); err != nil { + return nil, err + } + latestBundles := make([]types.LatestUuidBundle, 0, len(dstLatestBundles)) + for _, dbLub := range dstLatestBundles { + latestBundles = append(latestBundles, types.LatestUuidBundle{ + Uuid: dbLub.Uuid, + SigningAddress: common.HexToAddress(dbLub.SigningAddress), + BundleHash: common.HexToHash(dbLub.BundleHash), + BundleUUID: dbLub.BundleUUID, + }) + } + return latestBundles, nil +} diff --git a/flashbotsextra/database_test.go b/flashbotsextra/database_test.go new file mode 100644 index 0000000000..a9c2c79337 --- /dev/null +++ b/flashbotsextra/database_test.go @@ -0,0 +1,220 @@ +package flashbotsextra + +import ( + "math/big" + "os" + "testing" + "time" + + apiv1 "github.com/attestantio/go-builder-client/api/v1" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/stretchr/testify/require" +) + +func TestDatabaseBlockInsertion(t *testing.T) { + dsn := os.Getenv("FLASHBOTS_TEST_POSTGRES_DSN") + if dsn == "" { + t.Skip() + } + ds, err := NewDatabaseService(dsn) + require.NoError(t, err) + + _, err = ds.db.Exec("delete from built_blocks_bundles where block_id = (select block_id from built_blocks where hash = '0x9cc3ee47d091fea38c0187049cae56abe4e642eeb06c4832f06ec59f5dbce7ab')") + require.NoError(t, err) + + _, err = ds.db.Exec("delete from built_blocks_all_bundles where block_id = (select block_id from built_blocks where hash = '0x9cc3ee47d091fea38c0187049cae56abe4e642eeb06c4832f06ec59f5dbce7ab')") + require.NoError(t, err) + + _, err = ds.db.Exec("delete from built_blocks where hash = '0x9cc3ee47d091fea38c0187049cae56abe4e642eeb06c4832f06ec59f5dbce7ab'") + require.NoError(t, err) + + _, err = ds.db.Exec("delete from bundles where bundle_hash in ('0x0978000000000000000000000000000000000000000000000000000000000000', '0x1078000000000000000000000000000000000000000000000000000000000000', '0x0979000000000000000000000000000000000000000000000000000000000000', '0x1080000000000000000000000000000000000000000000000000000000000000')") + require.NoError(t, err) + + block := types.NewBlock( + &types.Header{ + ParentHash: common.HexToHash("0xafafafa"), + Number: big.NewInt(12), + GasLimit: uint64(10000), + GasUsed: uint64(1000), + Time: 16000000, + BaseFee: big.NewInt(7), + }, nil, nil, nil, nil) + blockProfit := big.NewInt(10) + + simBundle1 := types.SimulatedBundle{ + MevGasPrice: big.NewInt(9), + TotalEth: big.NewInt(11), + EthSentToCoinbase: big.NewInt(10), + TotalGasUsed: uint64(100), + OriginalBundle: types.MevBundle{ + Txs: types.Transactions{types.NewTransaction(uint64(50), common.Address{0x60}, big.NewInt(19), uint64(67), big.NewInt(43), []byte{})}, + BlockNumber: big.NewInt(12), + MinTimestamp: uint64(1000000), + RevertingTxHashes: []common.Hash{{0x10, 0x17}}, + Hash: common.Hash{0x09, 0x78}, + }, + } + + simBundle2 := types.SimulatedBundle{ + MevGasPrice: big.NewInt(90), + TotalEth: big.NewInt(110), + EthSentToCoinbase: big.NewInt(100), + TotalGasUsed: uint64(1000), + OriginalBundle: types.MevBundle{ + Txs: types.Transactions{types.NewTransaction(uint64(51), common.Address{0x61}, big.NewInt(109), uint64(167), big.NewInt(433), []byte{})}, + BlockNumber: big.NewInt(12), + MinTimestamp: uint64(1000020), + RevertingTxHashes: []common.Hash{{0x11, 0x17}}, + Hash: common.Hash{0x10, 0x78}, + }, + } + + var bundle2Id uint64 + ds.db.Get(&bundle2Id, "insert into bundles (bundle_hash, param_signed_txs, param_block_number, param_timestamp, received_timestamp, param_reverting_tx_hashes, coinbase_diff, total_gas_used, state_block_number, gas_fees, eth_sent_to_coinbase) values (:bundle_hash, :param_signed_txs, :param_block_number, :param_timestamp, :received_timestamp, :param_reverting_tx_hashes, :coinbase_diff, :total_gas_used, :state_block_number, :gas_fees, :eth_sent_to_coinbase) on conflict (bundle_hash, param_block_number) do nothing returning id", SimulatedBundleToDbBundle(&simBundle2)) + + simBundle3 := types.SimulatedBundle{ + MevGasPrice: big.NewInt(91), + TotalEth: big.NewInt(111), + EthSentToCoinbase: big.NewInt(101), + TotalGasUsed: uint64(101), + OriginalBundle: types.MevBundle{ + Txs: types.Transactions{types.NewTransaction(uint64(51), common.Address{0x62}, big.NewInt(20), uint64(68), big.NewInt(44), []byte{})}, + BlockNumber: big.NewInt(12), + MinTimestamp: uint64(1000021), + RevertingTxHashes: []common.Hash{{0x10, 0x18}}, + Hash: common.Hash{0x09, 0x79}, + }, + } + + simBundle4 := types.SimulatedBundle{ + MevGasPrice: big.NewInt(92), + TotalEth: big.NewInt(112), + EthSentToCoinbase: big.NewInt(102), + TotalGasUsed: uint64(1002), + OriginalBundle: types.MevBundle{ + Txs: types.Transactions{types.NewTransaction(uint64(52), common.Address{0x62}, big.NewInt(110), uint64(168), big.NewInt(434), []byte{})}, + BlockNumber: big.NewInt(12), + MinTimestamp: uint64(1000022), + RevertingTxHashes: []common.Hash{{0x11, 0x19}}, + Hash: common.Hash{0x10, 0x80}, + }, + } + + var bundle4Id uint64 + ds.db.Get(&bundle4Id, "insert into bundles (bundle_hash, param_signed_txs, param_block_number, param_timestamp, received_timestamp, param_reverting_tx_hashes, coinbase_diff, total_gas_used, state_block_number, gas_fees, eth_sent_to_coinbase) values (:bundle_hash, :param_signed_txs, :param_block_number, :param_timestamp, :received_timestamp, :param_reverting_tx_hashes, :coinbase_diff, :total_gas_used, :state_block_number, :gas_fees, :eth_sent_to_coinbase) on conflict (bundle_hash, param_block_number) do nothing returning id", SimulatedBundleToDbBundle(&simBundle4)) + + usedSbundle := types.UsedSBundle{ + Bundle: &types.SBundle{ + Inclusion: types.BundleInclusion{ + BlockNumber: 5, + MaxBlockNumber: 6, + }, + Body: []types.BundleBody{ + { + Tx: types.NewTransaction(uint64(53), common.Address{0x63}, big.NewInt(111), uint64(169), big.NewInt(435), []byte{})}, + }, + }, + Success: true, + } + + bidTrace := &apiv1.BidTrace{} + + ocAt := time.Now().Add(-time.Hour).UTC() + sealedAt := time.Now().Add(-30 * time.Minute).UTC() + ds.ConsumeBuiltBlock(block, blockProfit, ocAt, sealedAt, + []types.SimulatedBundle{simBundle1, simBundle2}, []types.SimulatedBundle{simBundle1, simBundle2, simBundle3, simBundle4}, + []types.UsedSBundle{usedSbundle}, bidTrace) + + var dbBlock BuiltBlock + require.NoError(t, ds.db.Get(&dbBlock, "select block_id, block_number, profit, slot, hash, gas_limit, gas_used, base_fee, parent_hash, timestamp, timestamp_datetime, orders_closed_at, sealed_at from built_blocks where hash = '0x9cc3ee47d091fea38c0187049cae56abe4e642eeb06c4832f06ec59f5dbce7ab'")) + require.Equal(t, BuiltBlock{ + BlockId: dbBlock.BlockId, + BlockNumber: 12, + Profit: "0.000000000000000010", + Slot: 0, + Hash: block.Hash().String(), + GasLimit: block.GasLimit(), + GasUsed: block.GasUsed(), + BaseFee: 7, + ParentHash: "0x000000000000000000000000000000000000000000000000000000000afafafa", + Timestamp: 16000000, + TimestampDatetime: dbBlock.TimestampDatetime, + OrdersClosedAt: dbBlock.OrdersClosedAt, + SealedAt: dbBlock.SealedAt, + }, dbBlock) + + require.True(t, dbBlock.TimestampDatetime.Equal(time.Unix(16000000, 0))) + require.Equal(t, ocAt.Truncate(time.Millisecond), dbBlock.OrdersClosedAt.UTC().Truncate(time.Millisecond)) + require.Equal(t, sealedAt.Truncate(time.Millisecond), dbBlock.SealedAt.UTC().Truncate(time.Millisecond)) + + var bundles []DbBundle + ds.db.Select(&bundles, "select bundle_hash, param_signed_txs, param_block_number, param_timestamp, param_reverting_tx_hashes, coinbase_diff, total_gas_used, state_block_number, gas_fees, eth_sent_to_coinbase, bundle_uuid from bundles order by param_timestamp") + require.Len(t, bundles, 4) + require.Equal(t, []DbBundle{SimulatedBundleToDbBundle(&simBundle1), SimulatedBundleToDbBundle(&simBundle2), SimulatedBundleToDbBundle(&simBundle3), SimulatedBundleToDbBundle(&simBundle4)}, bundles) + + var commitedBundles []string + require.NoError(t, ds.db.Select(&commitedBundles, "select b.bundle_hash as bundle_hash from built_blocks_bundles bbb inner join bundles b on b.id = bbb.bundle_id where bbb.block_id = $1 order by b.param_timestamp", dbBlock.BlockId)) + require.Len(t, commitedBundles, 2) + require.Equal(t, []string{simBundle1.OriginalBundle.Hash.String(), simBundle2.OriginalBundle.Hash.String()}, commitedBundles) + + var allBundles []string + require.NoError(t, ds.db.Select(&allBundles, "select b.bundle_hash as bundle_hash from built_blocks_all_bundles bbb inner join bundles b on b.id = bbb.bundle_id where bbb.block_id = $1 order by b.param_timestamp", dbBlock.BlockId)) + require.Len(t, allBundles, 4) + require.Equal(t, []string{simBundle1.OriginalBundle.Hash.String(), simBundle2.OriginalBundle.Hash.String(), simBundle3.OriginalBundle.Hash.String(), simBundle4.OriginalBundle.Hash.String()}, allBundles) + + var usedSbundles []DbUsedSBundle + require.NoError(t, ds.db.Select(&usedSbundles, "select hash, inserted from sbundle_builder_used where block_id = $1", dbBlock.BlockId)) + require.Len(t, usedSbundles, 1) + require.Equal(t, DbUsedSBundle{ + Hash: usedSbundle.Bundle.Hash().Bytes(), + Inserted: usedSbundle.Success, + }, usedSbundles[0]) +} + +func simpleTx(nonce uint64) *types.Transaction { + value := big.NewInt(1000000000000000) // in wei (0.001 eth) + gasLimit := uint64(21000) // in units + gasPrice := big.NewInt(1000000000) + + toAddress := common.HexToAddress("0x7777492a736CD894Cb12DFE5e944047499AEF7a0") + var data []byte + return types.NewTx(&types.LegacyTx{ + Nonce: nonce, + To: &toAddress, + Value: value, + Gas: gasLimit, + GasPrice: gasPrice, + Data: data, + }) +} + +func TestBundleUUIDHash(t *testing.T) { + tx1 := simpleTx(1) + tx2 := simpleTx(2) + bts1, err := tx1.MarshalBinary() + require.Nil(t, err) + bts2, err := tx2.MarshalBinary() + require.Nil(t, err) + _, _ = bts1, bts2 + t.Run("no reverts", func(t *testing.T) { + b := types.MevBundle{ + BlockNumber: big.NewInt(1), + Hash: common.HexToHash("0x135a7f22459b2102d51de2d6704512a03e1e2d2059c34bcbb659f4ba65e9f92c"), + } + + require.Equal(t, "5171315f-6ba4-52b2-866e-e2390d422d81", b.ComputeUUID().String()) + }) + t.Run("one revert", func(t *testing.T) { + b := types.MevBundle{ + BlockNumber: big.NewInt(1), + Hash: common.HexToHash("0x135a7f22459b2102d51de2d6704512a03e1e2d2059c34bcbb659f4ba65e9f92c"), + RevertingTxHashes: []common.Hash{ + tx1.Hash(), + }, + } + + require.Equal(t, "49dada39-6db2-500e-ae59-6cc18b2c19e0", b.ComputeUUID().String()) + }) +} diff --git a/flashbotsextra/database_types.go b/flashbotsextra/database_types.go new file mode 100644 index 0000000000..c034d6cfe8 --- /dev/null +++ b/flashbotsextra/database_types.go @@ -0,0 +1,111 @@ +package flashbotsextra + +import ( + "math/big" + "strings" + "time" + + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/log" + "github.com/google/uuid" +) + +type BuiltBlock struct { + BlockId uint64 `db:"block_id"` + BlockNumber uint64 `db:"block_number"` + Profit string `db:"profit"` + Slot uint64 `db:"slot"` + Hash string `db:"hash"` + GasLimit uint64 `db:"gas_limit"` + GasUsed uint64 `db:"gas_used"` + BaseFee uint64 `db:"base_fee"` + ParentHash string `db:"parent_hash"` + ProposerPubkey string `db:"proposer_pubkey"` + ProposerFeeRecipient string `db:"proposer_fee_recipient"` + BuilderPubkey string `db:"builder_pubkey"` + Timestamp uint64 `db:"timestamp"` + TimestampDatetime time.Time `db:"timestamp_datetime"` + OrdersClosedAt time.Time `db:"orders_closed_at"` + SealedAt time.Time `db:"sealed_at"` +} + +type BuiltBlockBundle struct { + BlockId uint64 `db:"block_id"` + BundleId *uint64 `db:"bundle_id"` + BlockNumber uint64 `db:"block_number"` + BundleHash string `db:"bundle_hash"` +} + +type DbBundle struct { + DbId uint64 `db:"id"` + BundleHash string `db:"bundle_hash"` + BundleUUID uuid.UUID `db:"bundle_uuid"` + + ParamSignedTxs string `db:"param_signed_txs"` + ParamBlockNumber uint64 `db:"param_block_number"` + ParamTimestamp *uint64 `db:"param_timestamp"` + ReceivedTimestamp time.Time `db:"received_timestamp"` + ParamRevertingTxHashes *string `db:"param_reverting_tx_hashes"` + + CoinbaseDiff string `db:"coinbase_diff"` + TotalGasUsed uint64 `db:"total_gas_used"` + StateBlockNumber uint64 `db:"state_block_number"` + GasFees string `db:"gas_fees"` + EthSentToCoinbase string `db:"eth_sent_to_coinbase"` +} + +type DbLatestUuidBundle struct { + Uuid uuid.UUID `db:"replacement_uuid"` + SigningAddress string `db:"signing_address"` + BundleHash string `db:"bundle_hash"` + BundleUUID uuid.UUID `db:"bundle_uuid"` +} + +type blockAndBundleId struct { + BlockId uint64 `db:"block_id"` + BundleId uint64 `db:"bundle_id"` +} + +type DbUsedSBundle struct { + BlockId uint64 `db:"block_id"` + Hash []byte `db:"hash"` + Inserted bool `db:"inserted"` +} + +var insertUsedSbundleQuery = ` +INSERT INTO sbundle_builder_used (block_id, hash, inserted) +VALUES (:block_id, :hash, :inserted) +ON CONFLICT (block_id, hash) DO NOTHING` + +func SimulatedBundleToDbBundle(bundle *types.SimulatedBundle) DbBundle { + revertingTxHashes := make([]string, len(bundle.OriginalBundle.RevertingTxHashes)) + for i, rTxHash := range bundle.OriginalBundle.RevertingTxHashes { + revertingTxHashes[i] = rTxHash.String() + } + paramRevertingTxHashes := strings.Join(revertingTxHashes, ",") + signedTxsStrings := make([]string, len(bundle.OriginalBundle.Txs)) + for i, tx := range bundle.OriginalBundle.Txs { + txBytes, err := tx.MarshalBinary() + if err != nil { + log.Error("could not marshal tx bytes", "err", err) + continue + } + signedTxsStrings[i] = hexutil.Encode(txBytes) + } + + return DbBundle{ + BundleHash: bundle.OriginalBundle.Hash.String(), + BundleUUID: bundle.OriginalBundle.ComputeUUID(), + ParamSignedTxs: strings.Join(signedTxsStrings, ","), + ParamBlockNumber: bundle.OriginalBundle.BlockNumber.Uint64(), + ParamTimestamp: &bundle.OriginalBundle.MinTimestamp, + ParamRevertingTxHashes: ¶mRevertingTxHashes, + + CoinbaseDiff: new(big.Rat).SetFrac(bundle.TotalEth, big.NewInt(1e18)).FloatString(18), + TotalGasUsed: bundle.TotalGasUsed, + StateBlockNumber: bundle.OriginalBundle.BlockNumber.Uint64(), + GasFees: new(big.Int).Mul(big.NewInt(int64(bundle.TotalGasUsed)), bundle.MevGasPrice).String(), + EthSentToCoinbase: new(big.Rat).SetFrac(bundle.EthSentToCoinbase, big.NewInt(1e18)).FloatString(18), + } +} diff --git a/flashbotsextra/fetcher.go b/flashbotsextra/fetcher.go new file mode 100644 index 0000000000..9b966dc7e1 --- /dev/null +++ b/flashbotsextra/fetcher.go @@ -0,0 +1,171 @@ +package flashbotsextra + +import ( + "context" + "errors" + "math/big" + "strings" + "time" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/eth" + "github.com/ethereum/go-ethereum/log" + "golang.org/x/crypto/sha3" +) + +type Fetcher interface { + Run() error +} + +type bundleFetcher struct { + db IDatabaseService + backend *eth.Ethereum + blockNumCh chan int64 + bundlesCh chan []types.MevBundle + shouldPushToTxPool bool // Added for testing +} + +func NewBundleFetcher(backend *eth.Ethereum, db IDatabaseService, blockNumCh chan int64, bundlesCh chan []types.MevBundle, shouldPushToTxPool bool) *bundleFetcher { + return &bundleFetcher{ + db: db, + backend: backend, + blockNumCh: blockNumCh, + bundlesCh: bundlesCh, + shouldPushToTxPool: shouldPushToTxPool, + } +} + +func (b *bundleFetcher) Run() { + log.Info("Start bundle fetcher") + if b.shouldPushToTxPool { + eventCh := make(chan core.ChainHeadEvent) + b.backend.BlockChain().SubscribeChainHeadEvent(eventCh) + pushBlockNum := func() { + for currentBlockNum := range eventCh { + b.blockNumCh <- currentBlockNum.Block.Header().Number.Int64() + } + } + addMevBundle := func() { + log.Info("Start receiving mev bundles") + for bundles := range b.bundlesCh { + b.backend.TxPool().AddMevBundles(bundles) + } + } + go pushBlockNum() + go addMevBundle() + } + pushMevBundles := func(bundles []DbBundle) { + mevBundles := make([]types.MevBundle, 0) + for _, bundle := range bundles { + mevBundle, err := b.dbBundleToMevBundle(bundle) + if err != nil { + log.Error("failed to convert db bundle to mev bundle", "err", err) + continue + } + mevBundles = append(mevBundles, *mevBundle) + } + if len(mevBundles) > 0 { + b.bundlesCh <- mevBundles + } + } + go b.fetchAndPush(context.Background(), pushMevBundles) +} + +func (b *bundleFetcher) GetLatestUuidBundles(ctx context.Context, blockNum int64) ([]types.LatestUuidBundle, error) { + return b.db.GetLatestUuidBundles(ctx, blockNum) +} + +func (b *bundleFetcher) fetchAndPush(ctx context.Context, pushMevBundles func(bundles []DbBundle)) { + var currentBlockNum int64 + lowPrioBundleTicker := time.NewTicker(time.Second * 2) + defer lowPrioBundleTicker.Stop() + + for { + select { + case currentBlockNum = <-b.blockNumCh: + ctxH, cancelH := context.WithTimeout(ctx, time.Second*3) + bundles, err := b.db.GetPriorityBundles(ctxH, currentBlockNum+1, true) + cancelH() + if err != nil { + log.Error("failed to fetch high prio bundles", "err", err) + continue + } + log.Debug("Fetching High prio bundles", "size", len(bundles), "currentlyBuiltBlockNum", currentBlockNum+1) + if len(bundles) != 0 { + pushMevBundles(bundles) + } + + case <-lowPrioBundleTicker.C: + if currentBlockNum == 0 { + continue + } + ctxL, cancelL := context.WithTimeout(ctx, time.Second*3) + bundles, err := b.db.GetPriorityBundles(ctxL, currentBlockNum+1, false) + cancelL() + if err != nil { + log.Error("failed to fetch low prio bundles", "err", err) + continue + } + log.Debug("Fetching low prio bundles", "len", len(bundles), "currentlyBuiltBlockNum", currentBlockNum+1) + if len(bundles) != 0 { + pushMevBundles(bundles) + } + case <-ctx.Done(): + close(b.bundlesCh) + return + } + } +} + +func (b *bundleFetcher) dbBundleToMevBundle(arg DbBundle) (*types.MevBundle, error) { + signedTxsStr := strings.Split(arg.ParamSignedTxs, ",") + if len(signedTxsStr) == 0 { + return nil, errors.New("bundle missing txs") + } + if arg.ParamBlockNumber == 0 { + return nil, errors.New("bundle missing blockNumber") + } + + var txs types.Transactions + for _, txStr := range signedTxsStr { + decodedTx, err := hexutil.Decode(txStr) + if err != nil { + log.Error("could not decode bundle tx", "id", arg.DbId, "err", err) + continue + } + tx := new(types.Transaction) + if err := tx.UnmarshalBinary(decodedTx); err != nil { + log.Error("could not unmarshal bundle decoded tx", "id", arg.DbId, "err", err) + continue + } + txs = append(txs, tx) + } + var paramRevertingTxHashes []string + if arg.ParamRevertingTxHashes != nil { + paramRevertingTxHashes = strings.Split(*arg.ParamRevertingTxHashes, ",") + } + revertingTxHashesStrings := paramRevertingTxHashes + revertingTxHashes := make([]common.Hash, len(revertingTxHashesStrings)) + for _, rTxHashStr := range revertingTxHashesStrings { + revertingTxHashes = append(revertingTxHashes, common.HexToHash(rTxHashStr)) + } + var minTimestamp uint64 + if arg.ParamTimestamp != nil { + minTimestamp = *arg.ParamTimestamp + } + bundleHasher := sha3.NewLegacyKeccak256() + for _, tx := range txs { + bundleHasher.Write(tx.Hash().Bytes()) + } + bundleHash := common.BytesToHash(bundleHasher.Sum(nil)) + return &types.MevBundle{ + Txs: txs, + BlockNumber: new(big.Int).SetUint64(arg.ParamBlockNumber), + MinTimestamp: minTimestamp, + RevertingTxHashes: revertingTxHashes, + Hash: bundleHash, + }, nil +} diff --git a/go.mod b/go.mod index 748a300274..a4f61f2ee9 100644 --- a/go.mod +++ b/go.mod @@ -8,15 +8,17 @@ require ( github.com/JekaMas/go-grpc-net-conn v0.0.0-20220708155319-6aff21f2d13d github.com/JekaMas/workerpool v1.1.8 github.com/VictoriaMetrics/fastcache v1.6.0 + github.com/attestantio/go-builder-client v0.3.2-0.20230623215408-1b5c4f7aaeaa + github.com/attestantio/go-eth2-client v0.18.3 github.com/aws/aws-sdk-go-v2 v1.2.0 github.com/aws/aws-sdk-go-v2/config v1.1.1 github.com/aws/aws-sdk-go-v2/credentials v1.1.1 github.com/aws/aws-sdk-go-v2/service/route53 v1.1.1 - github.com/btcsuite/btcd/btcec/v2 v2.2.0 + github.com/btcsuite/btcd/btcec/v2 v2.3.2 github.com/cespare/cp v1.1.1 github.com/cloudflare/cloudflare-go v0.14.0 github.com/cockroachdb/pebble v0.0.0-20230209160836-829675f94811 - github.com/consensys/gnark-crypto v0.9.1-0.20230105202408-1a7a29904a7c + github.com/consensys/gnark-crypto v0.11.2 github.com/cosmos/cosmos-sdk v0.46.2 github.com/davecgh/go-spew v1.1.1 github.com/deckarep/golang-set/v2 v2.1.0 @@ -27,6 +29,8 @@ require ( github.com/fatih/color v1.13.0 github.com/fjl/gencodec v0.0.0-20220412091415-8bb9e558978c github.com/fjl/memsize v0.0.0-20190710130421-bcb5799ab5e5 + github.com/flashbots/go-boost-utils v1.6.1-0.20230530114823-e5d0f8730a0f + github.com/flashbots/go-utils v0.5.0 github.com/fsnotify/fsnotify v1.6.0 github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08 github.com/gballet/go-verkle v0.0.0-20220902153445-097bd83b7732 @@ -34,10 +38,11 @@ require ( github.com/gofrs/flock v0.8.1 github.com/golang-jwt/jwt/v4 v4.3.0 github.com/golang/mock v1.6.0 - github.com/golang/protobuf v1.5.2 + github.com/golang/protobuf v1.5.3 github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb github.com/google/gofuzz v1.2.0 - github.com/google/uuid v1.3.0 + github.com/google/uuid v1.3.1 + github.com/gorilla/mux v1.8.0 github.com/gorilla/websocket v1.4.2 github.com/graph-gophers/graphql-go v1.3.0 github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 @@ -46,17 +51,19 @@ require ( github.com/hashicorp/hcl/v2 v2.10.1 github.com/heimdalr/dag v1.2.1 github.com/holiman/bloomfilter/v2 v2.0.3 - github.com/holiman/uint256 v1.2.2-0.20230321075855-87b91420868c + github.com/holiman/uint256 v1.2.3 github.com/huin/goupnp v1.0.3 github.com/imdario/mergo v0.3.11 github.com/influxdata/influxdb-client-go/v2 v2.4.0 github.com/influxdata/influxdb1-client v0.0.0-20220302092344-a9ab5670611c github.com/jackpal/go-nat-pmp v1.0.2 github.com/jedisct1/go-minisign v0.0.0-20190909160543-45766022959e + github.com/jmoiron/sqlx v1.3.5 github.com/json-iterator/go v1.1.12 github.com/julienschmidt/httprouter v1.3.0 github.com/karalabe/usb v0.0.2 github.com/kylelemons/godebug v1.1.0 + github.com/lib/pq v1.2.0 github.com/maticnetwork/crand v1.0.2 github.com/maticnetwork/heimdall v0.3.1-0.20230227104835-81bd1055b0bc github.com/maticnetwork/polyproto v0.0.3-0.20230216113155-340ea926ca53 @@ -71,17 +78,17 @@ require ( github.com/ryanuber/columnize v2.1.2+incompatible github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible github.com/status-im/keycard-go v0.2.0 - github.com/stretchr/testify v1.8.0 + github.com/stretchr/testify v1.8.4 github.com/supranational/blst v0.3.8-0.20220526154634-513d2456b344 github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 github.com/tendermint/tendermint v0.32.7 github.com/tyler-smith/go-bip39 v1.1.0 github.com/urfave/cli/v2 v2.17.2-0.20221006022127-8f469abc00aa - golang.org/x/crypto v0.1.0 - golang.org/x/exp v0.0.0-20230206171751-46f607a40771 - golang.org/x/sync v0.1.0 - golang.org/x/sys v0.6.0 - golang.org/x/text v0.8.0 + golang.org/x/crypto v0.13.0 + golang.org/x/exp v0.0.0-20230810033253-352e893a4cad + golang.org/x/sync v0.2.0 + golang.org/x/sys v0.12.0 + golang.org/x/text v0.13.0 golang.org/x/time v0.0.0-20220922220347-f3bd1da661af golang.org/x/tools v0.7.0 gopkg.in/natefinch/lumberjack.v2 v2.0.0 @@ -124,7 +131,7 @@ require ( github.com/consensys/bavard v0.1.13 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/crate-crypto/go-ipa v0.0.0-20220523130400-f11357ae11c7 // indirect - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 // indirect github.com/deepmap/oapi-codegen v1.8.2 // indirect github.com/dlclark/regexp2 v1.7.0 // indirect github.com/garslo/gogen v0.0.0-20170306192744-1d203ffc1f61 // indirect @@ -144,25 +151,25 @@ require ( github.com/opentracing/opentracing-go v1.2.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/prometheus/client_golang v1.14.0 // indirect + github.com/prometheus/client_golang v1.16.0 // indirect github.com/prometheus/client_model v0.3.0 // indirect - github.com/prometheus/common v0.39.0 // indirect - github.com/prometheus/procfs v0.9.0 // indirect - github.com/rogpeppe/go-internal v1.9.0 // indirect + github.com/prometheus/common v0.42.0 // indirect + github.com/prometheus/procfs v0.10.1 // indirect + github.com/rogpeppe/go-internal v1.11.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/tklauser/go-sysconf v0.3.5 // indirect github.com/tklauser/numcpus v0.2.2 // indirect github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect github.com/xsleonard/go-merkle v1.1.0 - go.opentelemetry.io/otel v1.2.0 + go.opentelemetry.io/otel v1.16.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.2.0 go.opentelemetry.io/otel/sdk v1.2.0 - go.uber.org/goleak v1.1.12 - golang.org/x/mod v0.9.0 // indirect - golang.org/x/net v0.8.0 // indirect + go.uber.org/goleak v1.2.0 + golang.org/x/mod v0.11.0 // indirect + golang.org/x/net v0.10.0 // indirect gonum.org/v1/gonum v0.11.0 google.golang.org/grpc v1.51.0 - google.golang.org/protobuf v1.28.1 + google.golang.org/protobuf v1.30.0 gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect gotest.tools v2.2.0+incompatible @@ -173,6 +180,7 @@ require ( require ( github.com/bartekn/go-bip39 v0.0.0-20171116152956-a05967ea095d // indirect github.com/bgentry/speakeasy v0.1.0 // indirect + github.com/bits-and-blooms/bitset v1.7.0 // indirect github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b // indirect github.com/btcsuite/btcd v0.22.3 // indirect github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce // indirect @@ -182,15 +190,18 @@ require ( github.com/cosmos/ledger-cosmos-go v0.10.3 // indirect github.com/cosmos/ledger-go v0.9.2 // indirect github.com/etcd-io/bbolt v1.3.3 // indirect + github.com/ferranbt/fastssz v0.1.3 // indirect github.com/gammazero/deque v0.2.1 // indirect github.com/go-kit/kit v0.10.0 // indirect github.com/go-logfmt/logfmt v0.5.1 // indirect + github.com/go-logr/logr v1.2.4 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/go-redis/redis v6.15.7+incompatible // indirect + github.com/goccy/go-yaml v1.9.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/gomodule/redigo v2.0.0+incompatible // indirect github.com/google/go-cmp v0.5.9 // indirect github.com/googleapis/gax-go/v2 v2.5.1 // indirect - github.com/gorilla/mux v1.8.0 // indirect github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect github.com/hashicorp/errwrap v1.0.0 // indirect github.com/hashicorp/go-multierror v1.0.0 // indirect @@ -200,14 +211,18 @@ require ( github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/jmhodges/levigo v1.0.0 // indirect github.com/kelseyhightower/envconfig v1.4.0 // indirect + github.com/klauspost/cpuid/v2 v2.1.2 // indirect github.com/libp2p/go-buffer-pool v0.0.2 // indirect github.com/magiconair/properties v1.8.6 // indirect + github.com/minio/sha256-simd v1.0.0 // indirect github.com/mitchellh/copystructure v1.0.0 // indirect github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect github.com/mitchellh/reflectwalk v1.0.0 // indirect github.com/posener/complete v1.1.1 // indirect + github.com/prysmaticlabs/go-bitfield v0.0.0-20210809151128-385d8c5e3fb7 // indirect github.com/rakyll/statik v0.1.7 // indirect github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect github.com/spf13/afero v1.8.2 // indirect github.com/spf13/cast v1.5.0 // indirect github.com/spf13/cobra v1.5.0 // indirect @@ -225,7 +240,11 @@ require ( github.com/xdg-go/scram v1.0.2 // indirect github.com/xdg-go/stringprep v1.0.2 // indirect github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect - golang.org/x/oauth2 v0.3.0 // indirect + go.opentelemetry.io/otel/metric v1.16.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.25.0 // indirect + golang.org/x/oauth2 v0.5.0 // indirect + golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect gotest.tools/v3 v3.5.0 // indirect ) @@ -241,7 +260,7 @@ require ( go.mongodb.org/mongo-driver v1.3.0 // indirect go.opencensus.io v0.23.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.2.0 // indirect - go.opentelemetry.io/otel/trace v1.2.0 + go.opentelemetry.io/otel/trace v1.16.0 go.opentelemetry.io/proto/otlp v0.10.0 // indirect google.golang.org/api v0.97.0 // indirect google.golang.org/appengine v1.6.7 // indirect diff --git a/go.sum b/go.sum index 4fa78d753c..2146fa49cb 100644 --- a/go.sum +++ b/go.sum @@ -150,6 +150,10 @@ github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmV github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310 h1:BUAU3CGlLvorLI26FmByPp2eC2qla6E1Tw+scpcg/to= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= +github.com/attestantio/go-builder-client v0.3.2-0.20230623215408-1b5c4f7aaeaa h1:WspfOBERk46bzU/Gew4WnKCcj/bOx3oVqHgPhG+6vUc= +github.com/attestantio/go-builder-client v0.3.2-0.20230623215408-1b5c4f7aaeaa/go.mod h1:DwesMTOqnCp4u+n3uZ+fWL8wwnSBZVD9VMIVPDR+AZE= +github.com/attestantio/go-eth2-client v0.18.3 h1:hUSYh+uMLyw4mJcXWcvrPLd8ozJl61aWMdx5Cpq9hxk= +github.com/attestantio/go-eth2-client v0.18.3/go.mod h1:KSVlZSW1A3jUg5H8O89DLtqxgJprRfTtI7k89fLdhu0= github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.29.15/go.mod h1:1KvfttTE3SPKMpo8g2c6jL3ZKfXtFvKscTgahTma5Xg= @@ -177,12 +181,15 @@ github.com/aws/smithy-go v1.1.0/go.mod h1:EzMw8dbp/YJL4A5/sbhGddag+NPT7q084agLbB github.com/aymerick/raymond v2.0.3-0.20180322193309-b565731e1464+incompatible/go.mod h1:osfaiScAUVup+UC9Nfq76eWqDhXlp+4UYaA8uhTBO6g= github.com/bartekn/go-bip39 v0.0.0-20171116152956-a05967ea095d h1:1aAija9gr0Hyv4KfQcRcwlmFIrhkDmIj2dz5bkg/s/8= github.com/bartekn/go-bip39 v0.0.0-20171116152956-a05967ea095d/go.mod h1:icNx/6QdFblhsEjZehARqbNumymUT/ydwlLojFdv7Sk= +github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bits-and-blooms/bitset v1.7.0 h1:YjAGVd3XmtK9ktAbX8Zg2g2PwLIMjGREZJHlV4j7NEo= +github.com/bits-and-blooms/bitset v1.7.0/go.mod h1:gIdJ4wp64HaoK2YrL1Q5/N7Y16edYb8uY+O0FJTyyDA= github.com/bmizerany/pat v0.0.0-20170815010413-6226ea591a40/go.mod h1:8rLXio+WjiTceGBHIoTvn60HIbs7Hm7bcHjyrSqYB9c= github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= @@ -195,8 +202,8 @@ github.com/btcsuite/btcd v0.22.0-beta/go.mod h1:9n5ntfhhHQBIhUvlhDvD3Qg6fRUj4jkN github.com/btcsuite/btcd v0.22.3 h1:kYNaWFvOw6xvqP0vR20RP1Zq1DVMBxEO8QN5d1/EfNg= github.com/btcsuite/btcd v0.22.3/go.mod h1:wqgTSL29+50LRkmOVknEdmt8ZojIzhuWvgu/iptuN7Y= github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= -github.com/btcsuite/btcd/btcec/v2 v2.2.0 h1:fzn1qaOt32TuLjFlkzYSsBC35Q3KUjT1SwPxiMSCF5k= -github.com/btcsuite/btcd/btcec/v2 v2.2.0/go.mod h1:U7MHm051Al6XmscBQ0BoNydpOTsFAn707034b5nY8zU= +github.com/btcsuite/btcd/btcec/v2 v2.3.2 h1:5n0X6hX0Zk+6omWcihdYvdAlGf2DfasC0GMf7DClJ3U= +github.com/btcsuite/btcd/btcec/v2 v2.3.2/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U= github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= @@ -261,8 +268,8 @@ github.com/consensys/bavard v0.1.8-0.20210406032232-f3452dc9b572/go.mod h1:Bpd0/ github.com/consensys/bavard v0.1.13 h1:oLhMLOFGTLdlda/kma4VOJazblc7IM5y5QPd2A/YjhQ= github.com/consensys/bavard v0.1.13/go.mod h1:9ItSMtA/dXMAiL7BG6bqW2m3NdSEObYWoH223nGHukI= github.com/consensys/gnark-crypto v0.4.1-0.20210426202927-39ac3d4b3f1f/go.mod h1:815PAHg3wvysy0SyIqanF8gZ0Y1wjk/hrDHD/iT88+Q= -github.com/consensys/gnark-crypto v0.9.1-0.20230105202408-1a7a29904a7c h1:llSLg4o9EgH3SrXky+Q5BqEYqV76NGKo07K5Ps2pIKo= -github.com/consensys/gnark-crypto v0.9.1-0.20230105202408-1a7a29904a7c/go.mod h1:CkbdF9hbRidRJYMRzmfX8TMOr95I2pYXRHF18MzRrvA= +github.com/consensys/gnark-crypto v0.11.2 h1:GJjjtWJ+db1xGao7vTsOgAOGgjfPe7eRGPL+xxMX0qE= +github.com/consensys/gnark-crypto v0.11.2/go.mod h1:v2Gy7L/4ZRosZ7Ivs+9SfUDr0f5UlG+EM5t7MPHiLuY= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= @@ -295,10 +302,11 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/deckarep/golang-set v1.8.0/go.mod h1:5nI87KwE7wgsBU1F4GKAw2Qod7p5kyS383rP6+o6qqo= github.com/deckarep/golang-set/v2 v2.1.0 h1:g47V4Or+DUdzbs8FxCCmgb6VYd+ptPAngjM6dtGktsI= github.com/deckarep/golang-set/v2 v2.1.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= -github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= +github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= github.com/deepmap/oapi-codegen v1.6.0/go.mod h1:ryDa9AgbELGeB+YEXE1dR53yAjHwFvE9iAUlWl9Al3M= github.com/deepmap/oapi-codegen v1.8.2 h1:SegyeYGcdi0jLLrpbCMoJxnUUn8GBXHsvr4rbzjuhfU= @@ -356,13 +364,20 @@ github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870/go.mod h1:5tD+ne github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= +github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= +github.com/ferranbt/fastssz v0.1.3 h1:ZI+z3JH05h4kgmFXdHuR1aWYsgrg7o+Fw7/NCzM16Mo= +github.com/ferranbt/fastssz v0.1.3/go.mod h1:0Y9TEd/9XuFlh7mskMPfXiI2Dkw4Ddg9EyXt1W7MRvE= github.com/fjl/gencodec v0.0.0-20220412091415-8bb9e558978c h1:CndMRAH4JIwxbW8KYq6Q+cGWcGHz0FjGR3QqcInWcW0= github.com/fjl/gencodec v0.0.0-20220412091415-8bb9e558978c/go.mod h1:AzA8Lj6YtixmJWL+wkKoBGsLWy9gFrAzi4g+5bCKwpY= github.com/fjl/memsize v0.0.0-20190710130421-bcb5799ab5e5 h1:FtmdgXiUlNeRsoNMFlKLDt+S+6hbjVMEW6RGQ7aUf7c= github.com/fjl/memsize v0.0.0-20190710130421-bcb5799ab5e5/go.mod h1:VvhXpOYNQvB+uIk2RvXzuaQtkQJzzIx6lSBe1xv7hi0= +github.com/flashbots/go-boost-utils v1.6.1-0.20230530114823-e5d0f8730a0f h1:bqjiOGi7eKSRC/RDLkh5voN/D4zIPOws3+IRU9J+U/o= +github.com/flashbots/go-boost-utils v1.6.1-0.20230530114823-e5d0f8730a0f/go.mod h1:fjoQ0NT/zd6LLVSSye+mCJhBemse7GW+06m+pxECETQ= +github.com/flashbots/go-utils v0.5.0 h1:ldjWta9B9//DJU2QcwRbErez3+1aKhSn6EoFc6d5kPY= +github.com/flashbots/go-utils v0.5.0/go.mod h1:LauDwifaRdSK0mS5X34GR59pJtUu1T/lOFNdff1BqtI= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= @@ -418,6 +433,11 @@ github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA= github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= +github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-martini/martini v0.0.0-20170121215854-22fa46961aab/go.mod h1:/P9AEU963A2AYjv4d1V5eVL1CQbEJq6aCNHDDjibzu8= github.com/go-ole/go-ole v1.2.5 h1:t4MGB5xEDZvXI+0rMjjsfBsD7yAgp/s9ZDkL1JndXwY= github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= @@ -425,6 +445,13 @@ github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34 github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-pdf/fpdf v0.5.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= github.com/go-pdf/fpdf v0.6.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho= +github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= +github.com/go-playground/validator/v10 v10.11.1 h1:prmOlTVv+YjZjmRmNSF3VmspqJIxJWXmqUsHwfTRRkQ= github.com/go-redis/redis v6.15.7+incompatible h1:3skhDh95XQMpnqeqNftPkQD9jL9e5e36z/1SUm6dy1U= github.com/go-redis/redis v6.15.7+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU= @@ -432,6 +459,8 @@ github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5Nq github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= +github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-stack/stack v1.8.1 h1:ntEHSVwIt7PNXNpgPmVfMrNhLtgjlmnZha2kOpuRiDw= github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4= @@ -465,6 +494,8 @@ github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY9 github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= +github.com/goccy/go-yaml v1.9.2 h1:2Njwzw+0+pjU2gb805ZC1B/uBuAs2VcZ3K+ZgHwDs7w= +github.com/goccy/go-yaml v1.9.2/go.mod h1:U/jl18uSupI5rdI2jmuCswEA2htH9eXfferR3KfscvA= github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= github.com/gofrs/uuid v3.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= @@ -521,8 +552,9 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= @@ -582,8 +614,9 @@ github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= +github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= github.com/googleapis/enterprise-certificate-proxy v0.1.0 h1:zO8WHNx/MYiAKJ3d5spxZXZE6KHmIQGQcAzwUzV7qQw= github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= @@ -657,9 +690,11 @@ github.com/holiman/big v0.0.0-20221017200358-a027dc42d04e/go.mod h1:j9cQbcqHQujT github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao= github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA= github.com/holiman/uint256 v1.2.0/go.mod h1:y4ga/t+u+Xwd7CpDgZESaRcWy0I7XMlTMA25ApIH5Jw= -github.com/holiman/uint256 v1.2.2-0.20230321075855-87b91420868c h1:DZfsyhDK1hnSS5lH8l+JggqzEleHteTYfutAiVlSUM8= -github.com/holiman/uint256 v1.2.2-0.20230321075855-87b91420868c/go.mod h1:SC8Ryt4n+UBbPbIBKaG9zbbDlp4jOru9xFZmPzLUTxw= +github.com/holiman/uint256 v1.2.3 h1:K8UWO1HUJpRMXBxbmaY1Y8IAMZC/RsKB+ArEnnK4l5o= +github.com/holiman/uint256 v1.2.3/go.mod h1:SC8Ryt4n+UBbPbIBKaG9zbbDlp4jOru9xFZmPzLUTxw= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/huandu/go-clone v1.6.0 h1:HMo5uvg4wgfiy5FoGOqlFLQED/VGRm2D9Pi8g1FXPGc= +github.com/huandu/go-clone/generic v1.6.0 h1:Wgmt/fUZ28r16F2Y3APotFD59sHk1p78K0XLdbUYN5U= github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw= github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= @@ -708,6 +743,8 @@ github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGw github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/jmhodges/levigo v1.0.0 h1:q5EC36kV79HWeTBWsod3mG11EgStG3qArTKcvlksN1U= github.com/jmhodges/levigo v1.0.0/go.mod h1:Q6Qx+uH3RAqyK4rFQroq9RL7mdkABMcfhEI+nNuzMJQ= +github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= +github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= @@ -757,6 +794,9 @@ github.com/klauspost/compress v1.15.15 h1:EF27CXIuDsYJ6mmvtBRlEuB2UVOqHG1tAXgZ7y github.com/klauspost/compress v1.15.15/go.mod h1:ZcK2JAFqKOpnBlxcLsJzYfrS9X1akm9fHZNnD9+Vo/4= github.com/klauspost/cpuid v0.0.0-20170728055534-ae7887de9fa5/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.1.2 h1:XhdX4fqAJUA0yj+kUwMavO0hHrSPAecYdYf1ZmxHvak= +github.com/klauspost/cpuid/v2 v2.1.2/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= github.com/klauspost/crc32 v0.0.0-20161016154125-cb6bfca970f6/go.mod h1:+ZoRqAPRLkC4NPOvfYeR5KNOrY6TD+/sAC3HXPZgDYg= github.com/klauspost/pgzip v1.0.2-0.20170402124221-0bf5dcad4ada/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -781,7 +821,11 @@ github.com/labstack/echo/v4 v4.5.0/go.mod h1:czIriw4a0C1dFun+ObrXp7ok03xON0N1awS github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= github.com/leanovate/gopter v0.2.9 h1:fQjYxZaynp97ozCzfOyOuAGOU4aU/z37zf/tOujFk7c= github.com/leanovate/gopter v0.2.9/go.mod h1:U2L/78B+KVFIx2VmW6onHJQzXtFb+p5y3y2Sh+Jxxv8= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.2.0 h1:LXpIM/LZ5xGFhOpXAQUIMM1HdyqzVYM13zNdjCEEcA0= +github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/libp2p/go-buffer-pool v0.0.2 h1:QNK2iAFa8gjAe1SPz6mHSMuCcjs+X1wlHzeOSqcmlfs= github.com/libp2p/go-buffer-pool v0.0.2/go.mod h1:MvaB6xw5vOrDl8rYZGLFdKAuk/hRoRZd1Vi32+RXyFM= github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= @@ -834,6 +878,8 @@ github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzp github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-sqlite3 v1.11.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg= +github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/mattn/go-tty v0.0.0-20180907095812-13ff1204f104/go.mod h1:XPvLUNfbS4fJH25nqRHfWLMa1ONC8Amw+mIA639KxkE= github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= @@ -842,6 +888,8 @@ github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfr github.com/mediocregopher/radix/v3 v3.4.2/go.mod h1:8FL3F6UQRXHXIBSPUs5h0RybMF8i4n7wVopoX3x7Bv8= github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/leAFZyRl6bYmGDlGc= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= +github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/cli v1.1.2 h1:PvH+lL2B7IQ101xQL63Of8yFS2y+aDlsFcsqNc+u/Kw= github.com/mitchellh/cli v1.1.2/go.mod h1:6iaV0fGdElS6dPBx0EApTxHrcWvmJphyh2n8YBLPPZ4= @@ -964,8 +1012,8 @@ github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeD github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= -github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw= -github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= +github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8= +github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= @@ -983,8 +1031,8 @@ github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt2 github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= -github.com/prometheus/common v0.39.0 h1:oOyhkDq05hPZKItWVBkJ6g6AtGxi+fy7F4JvUV8uhsI= -github.com/prometheus/common v0.39.0/go.mod h1:6XBZ7lYdLCbkAVhwRsWTZn+IN5AB9F/NXd5w0BbEX0Y= +github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= +github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= @@ -993,10 +1041,12 @@ github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+Gx github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= -github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= +github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg= +github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= github.com/prometheus/tsdb v0.10.0/go.mod h1:oi49uRhEe9dPUTlS3JRZOwJuVi6tmh10QSgwXEyGCt4= +github.com/prysmaticlabs/go-bitfield v0.0.0-20210809151128-385d8c5e3fb7 h1:0tVE4tdWQK9ZpYygoV7+vS6QkDvQVySboMVEIxBJmXw= +github.com/prysmaticlabs/go-bitfield v0.0.0-20210809151128-385d8c5e3fb7/go.mod h1:wmuf/mdK4VMD+jA9ThwcUKjg3a2XWM9cVfFYjDyY4j4= github.com/rakyll/statik v0.1.5/go.mod h1:OEi9wJV/fMUAGx1eNjq75DKDsJVuEv1U0oYdX6GX8Zs= github.com/rakyll/statik v0.1.7 h1:OF3QCZUuyPxuGEP7B4ypUa7sB/iHtqOTDYZXGM8KOdQ= github.com/rakyll/statik v0.1.7/go.mod h1:AlZONWzMtEnMs7W4e/1LURLiI49pIMmp6V9Unghqrcc= @@ -1013,8 +1063,9 @@ github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= -github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= @@ -1042,6 +1093,8 @@ github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPx github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/snikch/goodman v0.0.0-20171125024755-10e37e294daa/go.mod h1:oJyF+mSPHbB5mVY2iO9KV3pTt/QbIkGaO8gQ2WrDbP4= @@ -1083,8 +1136,8 @@ github.com/streadway/amqp v0.0.0-20200108173154-1c71cc93ed71/go.mod h1:AZpEONHx3 github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/testify v1.2.0/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= @@ -1093,8 +1146,9 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stumble/gorocksdb v0.0.3 h1:9UU+QA1pqFYJuf9+5p7z1IqdE5k0mma4UAeu2wmX8kA= github.com/stumble/gorocksdb v0.0.3/go.mod h1:v6IHdFBXk5DJ1K4FZ0xi+eY737quiiBxYtSWXadLybY= github.com/stvp/tempredis v0.0.0-20181119212430-b82af8480203 h1:QVqDTf3h2WHt08YuiTGPZLls0Wq99X9bWd0Q5ZSBesM= @@ -1128,12 +1182,14 @@ github.com/tklauser/numcpus v0.2.2 h1:oyhllyrScuYI6g+h/zUvNXNp1wy7x8qQy3t/piefld github.com/tklauser/numcpus v0.2.2/go.mod h1:x3qojaO3uyYt0i56EW/VUYs7uBvdl2fkfZFu0T9wgjM= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/trailofbits/go-fuzz-utils v0.0.0-20210901195358-9657fcfd256c h1:4WU+p200eLYtBsx3M5CKXvkjVdf5SC3W9nMg37y0TFI= github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8= github.com/tyler-smith/go-bip39 v1.1.0/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3CWg+kkNaLt55U= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/umbracle/gohashtree v0.0.2-alpha.0.20230207094856-5b775a815c10 h1:CQh33pStIp/E30b7TxDlXfM0145bn2e8boI30IxAhTg= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= @@ -1206,30 +1262,39 @@ go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.22.6/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= -go.opentelemetry.io/otel v1.2.0 h1:YOQDvxO1FayUcT9MIhJhgMyNO1WqoduiyvQHzGN0kUQ= go.opentelemetry.io/otel v1.2.0/go.mod h1:aT17Fk0Z1Nor9e0uisf98LrntPGMnk4frBO9+dkf69I= +go.opentelemetry.io/otel v1.16.0 h1:Z7GVAX/UkAXPKsy94IU+i6thsQS4nb7LviLpnaNeW8s= +go.opentelemetry.io/otel v1.16.0/go.mod h1:vl0h9NUa1D5s1nv3A5vZOYWn8av4K8Ml6JDeHrT/bx4= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.2.0 h1:xzbcGykysUh776gzD1LUPsNNHKWN0kQWDnJhn1ddUuk= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.2.0/go.mod h1:14T5gr+Y6s2AgHPqBMgnGwp04csUjQmYXFWPeiBoq5s= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.2.0 h1:VsgsSCDwOSuO8eMVh63Cd4nACMqgjpmAeJSIvVNneD0= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.2.0/go.mod h1:9mLBBnPRf3sf+ASVH2p9xREXVBvwib02FxcKnavtExg= +go.opentelemetry.io/otel/metric v1.16.0 h1:RbrpwVG1Hfv85LgnZ7+txXioPDoh6EdbZHo26Q3hqOo= +go.opentelemetry.io/otel/metric v1.16.0/go.mod h1:QE47cpOmkwipPiefDwo2wDzwJrlfxxNYodqc4xnGCo4= go.opentelemetry.io/otel/sdk v1.2.0 h1:wKN260u4DesJYhyjxDa7LRFkuhH7ncEVKU37LWcyNIo= go.opentelemetry.io/otel/sdk v1.2.0/go.mod h1:jNN8QtpvbsKhgaC6V5lHiejMoKD+V8uadoSafgHPx1U= -go.opentelemetry.io/otel/trace v1.2.0 h1:Ys3iqbqZhcf28hHzrm5WAquMkDHNZTUkw7KHbuNjej0= go.opentelemetry.io/otel/trace v1.2.0/go.mod h1:N5FLswTubnxKxOJHM7XZC074qpeEdLy3CgAVsdMucK0= +go.opentelemetry.io/otel/trace v1.16.0 h1:8JRpaObFoW0pxuVPapkgH8UhHQj+bJW8jJsCZEu5MQs= +go.opentelemetry.io/otel/trace v1.16.0/go.mod h1:Yt9vYq1SdNz3xdjZZK7wcXv1qv2pwLkqr2QVwea0ef0= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.opentelemetry.io/proto/otlp v0.10.0 h1:n7brgtEbDvXEgGyKKo8SobKT1e9FewlDtXzkVP5djoE= go.opentelemetry.io/proto/otlp v0.10.0/go.mod h1:zG20xCK0szZ1xdokeSOwEcmlXu+x9kkdRe6N1DhKcfU= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= -go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA= go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= +go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= +go.uber.org/zap v1.25.0 h1:4Hvk6GtkucQ790dqmj7l1eEnRdKm3k3ZUrUMS2d5+5c= +go.uber.org/zap v1.25.0/go.mod h1:JIAUzQIH94IC4fOJQm7gMmBJP5k7wQfdcnYdPoEXJYk= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -1254,8 +1319,9 @@ golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= +golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1272,8 +1338,8 @@ golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EH golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20200228211341-fcea875c7e85/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw= golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA= -golang.org/x/exp v0.0.0-20230206171751-46f607a40771 h1:xP7rWLUr1e1n2xkK5YB4LI0hPEy3LJC6Wk+D4pGlOJg= -golang.org/x/exp v0.0.0-20230206171751-46f607a40771/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= +golang.org/x/exp v0.0.0-20230810033253-352e893a4cad h1:g0bG7Z4uG+OgH2QDODnjp6ggkk1bJDsINcuWmJN1iJU= +golang.org/x/exp v0.0.0-20230810033253-352e893a4cad/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= @@ -1315,8 +1381,8 @@ golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs= -golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU= +golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1387,8 +1453,9 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1410,8 +1477,8 @@ golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= -golang.org/x/oauth2 v0.3.0 h1:6l90koy8/LaBLmLu8jpHeHexzMwEita0zFfYlggy2F8= -golang.org/x/oauth2 v0.3.0/go.mod h1:rQrIauxkUhJ6CuwEXwymO2/eh4xz2ZWF1nBkcxS+tGk= +golang.org/x/oauth2 v0.5.0 h1:HuArIo48skDwlrvM3sEdHXElYslAMsf3KwRkkW4MC4s= +golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1426,8 +1493,9 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI= +golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -1534,6 +1602,8 @@ golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1542,8 +1612,9 @@ golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20221013171732-95e765b1cc43/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -1561,8 +1632,9 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1657,8 +1729,9 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= -golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f h1:uF6paiQQebLeSXkrTqHqz0MXhXXS1KgF41eUdBNvxK0= golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= gonum.org/v1/gonum v0.0.0-20181121035319-3f7ecaa7e8ca/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= gonum.org/v1/gonum v0.6.0/go.mod h1:9mxDZsDKxgMAuccQkewq682L+0eCu4dCN2yonUJTCLU= @@ -1875,8 +1948,9 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/light/lightchain.go b/light/lightchain.go index cd39090f63..1c2d6bf250 100644 --- a/light/lightchain.go +++ b/light/lightchain.go @@ -675,3 +675,134 @@ func (lc *LightChain) EnableCheckFreq() { func (lc *LightChain) SubscribeStateSyncEvent(ch chan<- core.StateSyncEvent) event.Subscription { return lc.scope.Track(new(event.Feed).Subscribe(ch)) } + +/* +// ValidatePayload validates the payload of the block. +// It returns nil if the payload is valid, otherwise it returns an error. +// - `useBalanceDiffProfit` if set to false, proposer payment is assumed to be in the last transaction of the block +// otherwise we use proposer balance changes after the block to calculate proposer payment (see details in the code) +func (bc *LightChain) ValidatePayload(block *types.Block, feeRecipient common.Address, expectedProfit *big.Int, registeredGasLimit uint64, vmConfig vm.Config, useBalanceDiffProfit bool) error { + ctx, _ := context.WithCancel(context.Background()) + header := block.Header() + if err := bc.engine.VerifyHeader(bc, header, true); err != nil { + return err + } + + current := bc.CurrentBlock() + reorg, err := bc.forker.ReorgNeeded(current, header) + if err == nil && reorg { + return errors.New("block requires a reorg") + } + + parent := bc.GetHeader(block.ParentHash(), block.NumberU64()-1) + if parent == nil { + return errors.New("parent not found") + } + + calculatedGasLimit := utils.CalcGasLimit(parent.GasLimit, registeredGasLimit) + if calculatedGasLimit != header.GasLimit { + return errors.New("incorrect gas limit set") + } + + statedb, err := bc.StateAt(parent.Root) + if err != nil { + return err + } + + // The chain importer is starting and stopping trie prefetchers. If a bad + // block or other error is hit however, an early return may not properly + // terminate the background threads. This defer ensures that we clean up + // and dangling prefetcher, without defering each and holding on live refs. + defer statedb.StopPrefetcher() + + feeRecipientBalanceBefore := new(big.Int).Set(statedb.GetBalance(feeRecipient)) + + receipts, _, usedGas, err := bc.processor.Process(block, statedb, vmConfig, ctx) + if err != nil { + return err + } + + feeRecipientBalanceDelta := new(big.Int).Set(statedb.GetBalance(feeRecipient)) + feeRecipientBalanceDelta.Sub(feeRecipientBalanceDelta, feeRecipientBalanceBefore) + + if bc.Config().IsShanghai(header.Time) { + if header.WithdrawalsHash == nil { + return fmt.Errorf("withdrawals hash is missing") + } + // withdrawals hash and withdrawals validated later in ValidateBody + } else { + if header.WithdrawalsHash != nil { + return fmt.Errorf("withdrawals hash present before shanghai") + } + if block.Withdrawals() != nil { + return fmt.Errorf("withdrawals list present in block body before shanghai") + } + } + + if err := bc.validator.ValidateBody(block); err != nil { + return err + } + + if err := bc.validator.ValidateState(block, statedb, receipts, usedGas); err != nil { + return err + } + + // Validate proposer payment + + if useBalanceDiffProfit { + if feeRecipientBalanceDelta.Cmp(expectedProfit) >= 0 { + if feeRecipientBalanceDelta.Cmp(expectedProfit) > 0 { + log.Warn("builder claimed profit is lower than calculated profit", "expected", expectedProfit, "actual", feeRecipientBalanceDelta) + } + return nil + } + log.Warn("proposer payment not enough, trying last tx payment validation", "expected", expectedProfit, "actual", feeRecipientBalanceDelta) + } + + if len(receipts) == 0 { + return errors.New("no proposer payment receipt") + } + + lastReceipt := receipts[len(receipts)-1] + if lastReceipt.Status != types.ReceiptStatusSuccessful { + return errors.New("proposer payment not successful") + } + txIndex := lastReceipt.TransactionIndex + if txIndex+1 != uint(len(block.Transactions())) { + return fmt.Errorf("proposer payment index not last transaction in the block (%d of %d)", txIndex, len(block.Transactions())-1) + } + + paymentTx := block.Transaction(lastReceipt.TxHash) + if paymentTx == nil { + return errors.New("payment tx not in the block") + } + + paymentTo := paymentTx.To() + if paymentTo == nil || *paymentTo != feeRecipient { + return fmt.Errorf("payment tx not to the proposers fee recipient (%v)", paymentTo) + } + + if paymentTx.Value().Cmp(expectedProfit) != 0 { + return fmt.Errorf("inaccurate payment %s, expected %s", paymentTx.Value().String(), expectedProfit.String()) + } + + if len(paymentTx.Data()) != 0 { + return fmt.Errorf("malformed proposer payment, contains calldata") + } + + if paymentTx.GasPrice().Cmp(block.BaseFee()) != 0 { + return fmt.Errorf("malformed proposer payment, gas price not equal to base fee") + } + + if paymentTx.GasTipCap().Cmp(block.BaseFee()) != 0 && paymentTx.GasTipCap().Sign() != 0 { + return fmt.Errorf("malformed proposer payment, unexpected gas tip cap") + } + + if paymentTx.GasFeeCap().Cmp(block.BaseFee()) != 0 { + return fmt.Errorf("malformed proposer payment, unexpected gas fee cap") + } + + return nil +} + +*/ From f1c5c98f59350070d1e2f344817d1d99bcd6fb99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Nowosielski?= Date: Fri, 27 Oct 2023 15:56:03 +0200 Subject: [PATCH 3/7] Builder config and service registration --- builder/clique_builder.go | 31 +++++++++++++++---------------- builder/config.go | 2 +- builder/eth_service.go | 8 ++++++++ builder/service.go | 21 +++++---------------- internal/cli/server/config.go | 6 ++++++ internal/cli/server/server.go | 7 +++++++ 6 files changed, 42 insertions(+), 33 deletions(-) diff --git a/builder/clique_builder.go b/builder/clique_builder.go index e91d352d5c..b7cf0a3426 100644 --- a/builder/clique_builder.go +++ b/builder/clique_builder.go @@ -56,28 +56,28 @@ type CliqueBuilder struct { } type CliqueBuilderArgs struct { - sk *bls.SecretKey ds flashbotsextra.IDatabaseService relay IRelay blockTime time.Duration - proposerPubkey phase0.BLSPubKey proposerFeeRecipient bellatrix.ExecutionAddress proposerGasLimit uint64 - builderSigningDomain phase0.Domain - builderBlockResubmitInterval time.Duration eth IEthereumService limiter *rate.Limiter submissionOffsetFromEndOfSlot time.Duration validator *blockvalidation.BlockValidationAPI + builderSecretKey *bls.SecretKey + //proposerPubkey phase0.BLSPubKey + //builderSigningDomain phase0.Domain + //builderBlockResubmitInterval time.Duration } func NewCliqueBuilder(args CliqueBuilderArgs) (*CliqueBuilder, error) { - blsPk, err := bls.PublicKeyFromSecretKey(args.sk) + blsPk, err := bls.PublicKeyFromSecretKey(args.builderSecretKey) if err != nil { return nil, err } - pk, err := utils.BlsPublicKeyToPublicKey(blsPk) + builderPublicKey, err := utils.BlsPublicKeyToPublicKey(blsPk) if err != nil { return nil, err } @@ -86,9 +86,9 @@ func NewCliqueBuilder(args CliqueBuilderArgs) (*CliqueBuilder, error) { args.limiter = rate.NewLimiter(rate.Every(RateLimitIntervalDefault), RateLimitBurstDefault) } - if args.builderBlockResubmitInterval == 0 { - args.builderBlockResubmitInterval = BlockResubmitIntervalDefault - } + //if args.builderBlockResubmitInterval == 0 { + // args.builderBlockResubmitInterval = BlockResubmitIntervalDefault + //} if args.submissionOffsetFromEndOfSlot == 0 { args.submissionOffsetFromEndOfSlot = SubmissionOffsetFromEndOfSlotSecondsDefault @@ -100,26 +100,25 @@ func NewCliqueBuilder(args CliqueBuilderArgs) (*CliqueBuilder, error) { ds: args.ds, relay: args.relay, eth: args.eth, - proposerPubkey: args.proposerPubkey, proposerFeeRecipient: args.proposerFeeRecipient, proposerGasLimit: args.proposerGasLimit, - builderSecretKey: args.sk, - builderPublicKey: pk, - builderSigningDomain: args.builderSigningDomain, - builderResubmitInterval: args.builderBlockResubmitInterval, submissionOffsetFromEndOfSlot: args.submissionOffsetFromEndOfSlot, limiter: args.limiter, slotCtx: slotCtx, slotCtxCancel: slotCtxCancel, stop: make(chan struct{}, 1), + builderPublicKey: builderPublicKey, + builderSecretKey: args.builderSecretKey, + //proposerPubkey: args.proposerPubkey, + //builderSigningDomain: args.builderSigningDomain, + //builderResubmitInterval: args.builderBlockResubmitInterval, }, nil } func (cb *CliqueBuilder) Start() error { go func() { c := make(chan core.ChainHeadEvent) - //TODO: How to connect to Bor server? - //cb.eth.BlockChain().SubscribeChainHeadEvent(c) + cb.eth.BlockChain().SubscribeChainHeadEvent(c) for { select { case <-cb.stop: diff --git a/builder/config.go b/builder/config.go index 2bef45e542..798263363b 100644 --- a/builder/config.go +++ b/builder/config.go @@ -47,7 +47,7 @@ var DefaultConfig = Config{ GenesisForkVersion: "0x00000000", BellatrixForkVersion: "0x02000000", GenesisValidatorsRoot: "0x0000000000000000000000000000000000000000000000000000000000000000", - BeaconEndpoints: []string{"http://127.0.0.1:5052"}, + BeaconEndpoints: []string{}, RemoteRelayEndpoint: "", SecondaryRemoteRelayEndpoints: nil, ValidationBlocklist: "", diff --git a/builder/eth_service.go b/builder/eth_service.go index 26e3cf9379..33417e8061 100644 --- a/builder/eth_service.go +++ b/builder/eth_service.go @@ -7,6 +7,7 @@ import ( "github.com/ethereum/go-ethereum/beacon/engine" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/eth" "github.com/ethereum/go-ethereum/log" @@ -19,6 +20,7 @@ type IEthereumService interface { GetBlockByHash(hash common.Hash) *types.Block Config() *params.ChainConfig Synced() bool + BlockChain() *core.BlockChain } type testEthereumService struct { @@ -42,6 +44,8 @@ func (t *testEthereumService) Config() *params.ChainConfig { return params.TestC func (t *testEthereumService) Synced() bool { return t.synced } +func (t *testEthereumService) BlockChain() *core.BlockChain { return nil } + type EthereumService struct { eth *eth.Ethereum } @@ -102,3 +106,7 @@ func (s *EthereumService) Config() *params.ChainConfig { func (s *EthereumService) Synced() bool { return s.eth.Synced() } + +func (s *EthereumService) BlockChain() *core.BlockChain { + return s.eth.BlockChain() +} diff --git a/builder/service.go b/builder/service.go index 23f57c71cb..96e0744fb7 100644 --- a/builder/service.go +++ b/builder/service.go @@ -200,17 +200,6 @@ func Register(stack *node.Node, backend *eth.Ethereum, cfg *Config) error { // BuilderRateLimitMaxBurst is set to builder.RateLimitBurstDefault by default if not specified limiter := rate.NewLimiter(rate.Every(duration), cfg.BuilderRateLimitMaxBurst) - var builderRateLimitInterval time.Duration - if cfg.BuilderRateLimitResubmitInterval != "" { - d, err := time.ParseDuration(cfg.BuilderRateLimitResubmitInterval) - if err != nil { - return fmt.Errorf("error parsing builder rate limit resubmit interval - %v", err) - } - builderRateLimitInterval = d - } else { - builderRateLimitInterval = RateLimitIntervalDefault - } - var submissionOffset time.Duration if offset := cfg.BuilderSubmissionOffset; offset != 0 { if offset < 0 { @@ -242,19 +231,19 @@ func Register(stack *node.Node, backend *eth.Ethereum, cfg *Config) error { if err != nil { return errors.New("incorrect builder API secret key provided") } - proposerPubKey, err := bls.PublicKeyFromBytes(cfg.ProposerPubkey) + //proposerPubKey, err := bls.PublicKeyFromBytes(cfg.ProposerPubkey) builderArgs := CliqueBuilderArgs{ - sk: builderSk, + builderSecretKey: builderSk, ds: ds, eth: ethereumService, relay: relay, - proposerPubkey: proposerPubKey, - builderSigningDomain: builderSigningDomain, - builderBlockResubmitInterval: builderRateLimitInterval, submissionOffsetFromEndOfSlot: submissionOffset, limiter: limiter, validator: validator, + //proposerPubkey: proposerPubKey, + //builderSigningDomain: builderSigningDomain, + //builderBlockResubmitInterval: builderRateLimitInterval, } builderBackend, err := NewCliqueBuilder(builderArgs) diff --git a/internal/cli/server/config.go b/internal/cli/server/config.go index 0a9c7dd704..973395a648 100644 --- a/internal/cli/server/config.go +++ b/internal/cli/server/config.go @@ -22,6 +22,7 @@ import ( "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/accounts/keystore" + "github.com/ethereum/go-ethereum/builder" "github.com/ethereum/go-ethereum/cmd/utils" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/fdlimit" @@ -140,6 +141,9 @@ type Config struct { // Pprof has the pprof related settings Pprof *PprofConfig `hcl:"pprof,block" toml:"pprof,block"` + + // Flashbots builder + Builder *builder.Config `hcl:"builder,block" toml:"builder,block"` } type LoggingConfig struct { @@ -595,6 +599,7 @@ type ParallelEVMConfig struct { } func DefaultConfig() *Config { + builderCfg := builder.DefaultConfig return &Config{ Chain: "mainnet", Identity: Hostname(), @@ -787,6 +792,7 @@ func DefaultConfig() *Config { Enable: true, SpeculativeProcesses: 8, }, + Builder: &builderCfg, } } diff --git a/internal/cli/server/server.go b/internal/cli/server/server.go index 171b7e167e..b5c7b431ad 100644 --- a/internal/cli/server/server.go +++ b/internal/cli/server/server.go @@ -24,6 +24,7 @@ import ( "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/accounts/keystore" + "github.com/ethereum/go-ethereum/builder" "github.com/ethereum/go-ethereum/cmd/utils" "github.com/ethereum/go-ethereum/consensus/beacon" //nolint:typecheck "github.com/ethereum/go-ethereum/consensus/bor" //nolint:typecheck @@ -278,6 +279,12 @@ func NewServer(config *Config, opts ...serverOption) (*Server, error) { } } + if config.Builder.Enabled { + if err := builder.Register(stack, srv.backend, config.Builder); err != nil { + return nil, err + } + } + if err := srv.setupMetrics(config.Telemetry, config.Identity); err != nil { return nil, err } From 76171825abb74d28cf37cc49ebc198bff6be33db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Nowosielski?= Date: Fri, 3 Nov 2023 15:53:14 +0100 Subject: [PATCH 4/7] make the builder to work --- builder/builder.go | 2 +- builder/clique_builder.go | 20 ++- builder/local_relay.go | 35 ++++ builder/service.go | 3 + eth/catalyst/api.go | 2 +- packaging/localnet/README.md | 4 +- packaging/localnet/config/node1_config.toml | 4 + .../localnet/config/node2_builder_config.toml | 159 ++++++++++++++++++ 8 files changed, 218 insertions(+), 11 deletions(-) create mode 100644 packaging/localnet/config/node2_builder_config.toml diff --git a/builder/builder.go b/builder/builder.go index 44201bcdeb..ce8eb9842c 100644 --- a/builder/builder.go +++ b/builder/builder.go @@ -375,7 +375,7 @@ func (b *Builder) OnPayloadAttribute(attrs *types.BuilderPayloadAttributes) erro b.slotCtxCancel() } - slotCtx, slotCtxCancel := context.WithTimeout(context.Background(), 12*time.Second) + slotCtx, slotCtxCancel := context.WithTimeout(context.Background(), 2*time.Second) b.slotAttrs = *attrs b.slotCtx = slotCtx b.slotCtxCancel = slotCtxCancel diff --git a/builder/clique_builder.go b/builder/clique_builder.go index b7cf0a3426..700ef1ea37 100644 --- a/builder/clique_builder.go +++ b/builder/clique_builder.go @@ -66,9 +66,9 @@ type CliqueBuilderArgs struct { submissionOffsetFromEndOfSlot time.Duration validator *blockvalidation.BlockValidationAPI builderSecretKey *bls.SecretKey + builderBlockResubmitInterval time.Duration //proposerPubkey phase0.BLSPubKey //builderSigningDomain phase0.Domain - //builderBlockResubmitInterval time.Duration } func NewCliqueBuilder(args CliqueBuilderArgs) (*CliqueBuilder, error) { @@ -86,9 +86,9 @@ func NewCliqueBuilder(args CliqueBuilderArgs) (*CliqueBuilder, error) { args.limiter = rate.NewLimiter(rate.Every(RateLimitIntervalDefault), RateLimitBurstDefault) } - //if args.builderBlockResubmitInterval == 0 { - // args.builderBlockResubmitInterval = BlockResubmitIntervalDefault - //} + if args.builderBlockResubmitInterval == 0 { + args.builderBlockResubmitInterval = BlockResubmitIntervalDefault + } if args.submissionOffsetFromEndOfSlot == 0 { args.submissionOffsetFromEndOfSlot = SubmissionOffsetFromEndOfSlotSecondsDefault @@ -109,9 +109,10 @@ func NewCliqueBuilder(args CliqueBuilderArgs) (*CliqueBuilder, error) { stop: make(chan struct{}, 1), builderPublicKey: builderPublicKey, builderSecretKey: args.builderSecretKey, + builderResubmitInterval: args.builderBlockResubmitInterval, + blockTime: 8 * time.Second, //proposerPubkey: args.proposerPubkey, //builderSigningDomain: args.builderSigningDomain, - //builderResubmitInterval: args.builderBlockResubmitInterval, }, nil } @@ -160,8 +161,11 @@ func (cb *CliqueBuilder) onChainHeadEvent(block *types.Block) error { cb.slotCtx = slotCtx cb.slotCtxCancel = slotCtxCancel + // [pnowosie] nasty hack: I need to tweak the block's timestamp in order to make it pass + // see: catalyst/api.go:540 "Invalid timestamp" error + blockTime := block.Header().Time + 1 attrs := &types.BuilderPayloadAttributes{ - Timestamp: hexutil.Uint64(block.Header().Time), + Timestamp: hexutil.Uint64(blockTime), Random: common.Hash{}, // unused SuggestedFeeRecipient: common.Address{}, // unused Slot: block.NumberU64() + 1, @@ -204,6 +208,7 @@ func (cb *CliqueBuilder) runBuildingJob(slotCtx context.Context, attrs *types.Bu if err != nil { log.Error("could not run sealed block hook", "err", err) } else { + log.Info("Queued block for submission", "slot", attrs.Slot, "parent", attrs.HeadHash, "payloadTimestamp", uint64(attrs.Timestamp), "blockHash", queueBestEntry.block.Hash()) queueLastSubmittedHash = queueBestEntry.block.Hash() } } @@ -213,7 +218,7 @@ func (cb *CliqueBuilder) runBuildingJob(slotCtx context.Context, attrs *types.Bu // Avoid submitting early into a given slot. For example if slots have 12 second interval, submissions should // not begin until 8 seconds into the slot. slotTime := time.Unix(int64(attrs.Timestamp), 0).UTC() - slotSubmitStartTime := slotTime.Add(-cb.submissionOffsetFromEndOfSlot) + slotSubmitStartTime := slotTime.Add(-1 * time.Second) // Empties queue, submits the best block for current job with rate limit (global for all jobs) go runResubmitLoop(ctx, cb.limiter, queueSignal, submitBestBlock, slotSubmitStartTime) @@ -230,6 +235,7 @@ func (cb *CliqueBuilder) runBuildingJob(slotCtx context.Context, attrs *types.Bu queueMu.Lock() defer queueMu.Unlock() + log.Info("Queue best entry", "slot", attrs.Slot, "parent", attrs.HeadHash, "blockHash", block.Hash(), "best found", queueLastSubmittedHash) if block.Hash() != queueLastSubmittedHash { queueBestEntry = blockQueueEntry{ block: block, diff --git a/builder/local_relay.go b/builder/local_relay.go index 9c2abef78c..7e57bf1d68 100644 --- a/builder/local_relay.go +++ b/builder/local_relay.go @@ -302,6 +302,41 @@ func (r *LocalRelay) handleGetHeader(w http.ResponseWriter, req *http.Request) { } } +func (r *LocalRelay) handleGetBlock(w http.ResponseWriter, req *http.Request) { + vars := mux.Vars(req) + parentHashHex := vars["parent_hash"] + + r.bestDataLock.Lock() + bestHeader := r.bestHeader + bestPayload := r.bestPayload + r.bestDataLock.Unlock() + + if bestHeader == nil || bestPayload == nil { + respondError(w, http.StatusBadRequest, "no payloads") + return + } + + if bestHeader == nil || bestHeader.ParentHash.String() != parentHashHex { + respondError(w, http.StatusBadRequest, "unknown payload") + return + } + + response := &api.VersionedExecutionPayload{ + Version: consensusspec.DataVersionBellatrix, + Bellatrix: bestPayload, + } + + log.Info("sending block", "block", bestPayload.BlockHash.String(), "payload", bestPayload) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + if err := json.NewEncoder(w).Encode(response); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + respondError(w, http.StatusInternalServerError, "internal server error") + return + } +} + func (r *LocalRelay) handleGetPayload(w http.ResponseWriter, req *http.Request) { payload := new(apiv1bellatrix.SignedBlindedBeaconBlock) if err := json.NewDecoder(req.Body).Decode(&payload); err != nil { diff --git a/builder/service.go b/builder/service.go index 96e0744fb7..cb175129ce 100644 --- a/builder/service.go +++ b/builder/service.go @@ -28,6 +28,7 @@ const ( _PathRegisterValidator = "/eth/v1/builder/validators" _PathGetHeader = "/eth/v1/builder/header/{slot:[0-9]+}/{parent_hash:0x[a-fA-F0-9]+}/{pubkey:0x[a-fA-F0-9]+}" _PathGetPayload = "/eth/v1/builder/blinded_blocks" + _PathGetBlock = "/eth/v1/builder/block/{parent_hash:0x[a-fA-F0-9]+}" ) type Service struct { @@ -67,6 +68,7 @@ func getRouter(localRelay *LocalRelay) http.Handler { router.HandleFunc(_PathRegisterValidator, localRelay.handleRegisterValidator).Methods(http.MethodPost) router.HandleFunc(_PathGetHeader, localRelay.handleGetHeader).Methods(http.MethodGet) router.HandleFunc(_PathGetPayload, localRelay.handleGetPayload).Methods(http.MethodPost) + router.HandleFunc(_PathGetBlock, localRelay.handleGetBlock).Methods(http.MethodGet) // Add logging and return router loggedRouter := httplogger.LoggingMiddleware(router) @@ -126,6 +128,7 @@ func NewService(listenAddr string, localRelay *LocalRelay, builder IBuilder) *Se } func Register(stack *node.Node, backend *eth.Ethereum, cfg *Config) error { + fmt.Println("Builder service is registered") envBuilderSkBytes, err := hexutil.Decode(cfg.BuilderSecretKey) if err != nil { return errors.New("incorrect builder API secret key provided") diff --git a/eth/catalyst/api.go b/eth/catalyst/api.go index a6a0a08d2e..98546fcff2 100644 --- a/eth/catalyst/api.go +++ b/eth/catalyst/api.go @@ -537,7 +537,7 @@ func (api *ConsensusAPI) newPayload(params engine.ExecutableData) (engine.Payloa } if block.Time() <= parent.Time() { - log.Warn("Invalid timestamp", "parent", block.Time(), "block", block.Time()) + log.Warn("Invalid timestamp", "parent", parent.Time(), "block", block.Time()) return api.invalid(errors.New("invalid timestamp"), parent.Header()), nil } // Another cornercase: if the node is in snap sync mode, but the CL client diff --git a/packaging/localnet/README.md b/packaging/localnet/README.md index 3f37a0937c..8b92f9d0b4 100644 --- a/packaging/localnet/README.md +++ b/packaging/localnet/README.md @@ -24,9 +24,9 @@ cp ./config ${ROOTDIR}/ 6. Run ``` cd ${ROOTDIR} -./bin/bor --config config/node1_config.toml +./bin/bor server --config config/node1_config.toml # in another terminal: -./bin/bor --config config/node2_config.toml +./bin/bbuilder server --config config/node2_builder_config.toml 2>&1 | tee -a run.log ``` ## Testing the network diff --git a/packaging/localnet/config/node1_config.toml b/packaging/localnet/config/node1_config.toml index 71db10d9a8..3ba334bb18 100644 --- a/packaging/localnet/config/node1_config.toml +++ b/packaging/localnet/config/node1_config.toml @@ -142,3 +142,7 @@ devfakeauthor = true # [developer] # dev = false # period = 0 + +#[mev] +# enabled = true +# endpoint = ":28545" \ No newline at end of file diff --git a/packaging/localnet/config/node2_builder_config.toml b/packaging/localnet/config/node2_builder_config.toml new file mode 100644 index 0000000000..42462dd11f --- /dev/null +++ b/packaging/localnet/config/node2_builder_config.toml @@ -0,0 +1,159 @@ +# NOTE: Update and uncomment: `keystore`, `password`, and `unlock` fields. + +chain = "./config/genesis.json" +identity = "node2.local" +# [verbosity] +# LvlError = 1, LvlWarn = 2, LvlInfo = 3, LvlDebug = 4, LvlTrace = 5 +verbosity = 3 +datadir = "./data/node_2" +# ancient = "" +keystore = "./data/keystore" +# "rpc.batchlimit" = 100 +# "rpc.returndatalimit" = 100000 +syncmode = "full" +# gcmode = "full" +# snapshot = true +# ethstats = "" + +# Important otherwise we'd need to have a signer as part of block 0's validator set +devfakeauthor = true + +# ["eth.requiredblocks"] + +[p2p] + maxpeers = 5 + port = 30305 + # maxpendpeers = 50 + # bind = "0.0.0.0" + # nodiscover = false + # nat = "any" + [p2p.discovery] + # v5disc = false + bootnodes = ["enode://308dcb11dea3d17851b35ed0188bf6f500cb8a873110c7db5fe28fb9ecb8b6599960253d07639ca7767e99484a56140bbf8e68c88dd73e066b16e47bd7c47833@127.0.0.1:30303"] + # bootnodesv4 = [] + # bootnodesv5 = [] + # static-nodes = [] + # trusted-nodes = [] + # dns = [] + +[heimdall] + "bor.without" = true + # url = "http://localhost:1317" + # grpc-address = "" + +[txpool] + nolocals = true + accountslots = 16 + globalslots = 131072 + accountqueue = 64 + globalqueue = 131072 + lifetime = "1h30m0s" + # locals = [] + # journal = "" + # rejournal = "1h0m0s" + # pricelimit = 1 + # pricebump = 10 + +[miner] + mine = true + gaslimit = 30000000 + gasprice = "1000000" + etherbase = "15d34AAf54267DB7D7c367839AAf71A00a2C6A65" + # extradata = "" + +[jsonrpc] + ipcpath = "./ipc/node_2/bor.ipc" + # ipcdisable = false + # gascap = 50000000 + # txfeecap = 5.0 + [jsonrpc.http] + enabled = true + port = 8555 + host = "0.0.0.0" + api = ["eth", "net", "web3", "txpool", "bor", "personal"] + vhosts = ["*"] + corsdomain = ["*"] + # prefix = "" + # [jsonrpc.ws] + # enabled = false + # port = 8546 + # prefix = "" + # host = "localhost" + # api = ["web3", "net"] + # origins = ["*"] + # [jsonrpc.graphql] + # enabled = false + # port = 0 + # prefix = "" + # host = "" + # vhosts = ["*"] + # corsdomain = ["*"] + # [jsonrpc.timeouts] + # read = "30s" + # write = "30s" + # idle = "2m0s" + +# [gpo] +# blocks = 20 +# percentile = 60 +# maxprice = "5000000000000" +# ignoreprice = "2" + +[telemetry] + metrics = false + # expensive = false + # prometheus-addr = "" + # opencollector-endpoint = "" + # [telemetry.influx] + # influxdb = false + # endpoint = "" + # database = "" + # username = "" + # password = "" + # influxdbv2 = false + # token = "" + # bucket = "" + # organization = "" + # [telemetry.influx.tags] + +# [cache] + # cache = 1024 + # gc = 25 + # snapshot = 10 + # database = 50 + # trie = 15 + # journal = "triecache" + # rejournal = "1h0m0s" + # noprefetch = false + # preimages = false + # txlookuplimit = 2350000 + # timeout = "1h0m0s" + +[accounts] + allow-insecure-unlock = true + password = "./data/password.txt" + unlock = ["3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", "15d34AAf54267DB7D7c367839AAf71A00a2C6A65"] + # lightkdf = true + disable-bor-wallet = false + +[grpc] + addr = ":3232" + +[builder] + enabled = true + builderSecretKey = "0x000000e741f29701f8e30f5de6350766c020cb80768a0ff01e6838ffd2000000" + enableValidatorChecks = false + enableLocalRelay = true + disableBundleFetcher = true + dryRun = false + listenAddr = ":28545" + # ignoreLatePayloadAttributes = false + # builderSecretKey = "0x2fc12ae741f29701f8e30f5de6350766c020cb80768a0ff01e6838ffd2431e11" + # relaySecretKey = "0x2fc12ae741f29701f8e30f5de6350766c020cb80768a0ff01e6838ffd2431e11" + # genesisForkVersion = "0x00000000" + # bellatrixForkVersion = "0x02000000" + # genesisValidatorsRoot = "0x0000000000000000000000000000000000000000000000000000000000000000" + +# [developer] + # dev = false + # period = 0 From 9a18a9268be0f1786a40aa462ecb8fefa8ab0bbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Nowosielski?= Date: Fri, 17 Nov 2023 08:35:36 +0100 Subject: [PATCH 5/7] fix not imported builder signing key --- builder/local_relay.go | 6 ++++++ internal/cli/server/config.go | 10 ++++++++++ 2 files changed, 16 insertions(+) diff --git a/builder/local_relay.go b/builder/local_relay.go index 7e57bf1d68..2c782cd364 100644 --- a/builder/local_relay.go +++ b/builder/local_relay.go @@ -311,6 +311,12 @@ func (r *LocalRelay) handleGetBlock(w http.ResponseWriter, req *http.Request) { bestPayload := r.bestPayload r.bestDataLock.Unlock() + // TODO: [pnowosie] remove or lower severity + log.Info("================= Req received ===================", "parent", parentHashHex) + header := fmt.Sprintf("Parent : %s\n, BlockNumber : %d\n, BlockHash : %s\n, Timestamp : %d\n", + bestHeader.ParentHash, bestHeader.BlockNumber, bestHeader.BlockHash, bestHeader.Timestamp) + log.Info("👉 Best Header", "", header) + if bestHeader == nil || bestPayload == nil { respondError(w, http.StatusBadRequest, "no payloads") return diff --git a/internal/cli/server/config.go b/internal/cli/server/config.go index 973395a648..971da03f07 100644 --- a/internal/cli/server/config.go +++ b/internal/cli/server/config.go @@ -1194,6 +1194,16 @@ func (c *Config) buildEth(stack *node.Node, accountManager *accounts.Manager) (* n.DatabaseFreezer = c.Ancient } + // Builder + if c.Builder.Enabled { + key := c.Builder.BuilderSecretKey + if key, err := crypto.HexToECDSA(strings.TrimPrefix(key, "0x")); err != nil { + log.Error("Error parsing builder signing key from builder config", "err", err) + } else { + n.Miner.BuilderTxSigningKey = key + } + } + return &n, nil } From 464f375a74b9b5de951c4723fa1175ab79fdb55e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Nowosielski?= Date: Mon, 20 Nov 2023 17:09:19 +0100 Subject: [PATCH 6/7] little fix to relay --- builder/local_relay.go | 10 +++++----- builder/service.go | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/builder/local_relay.go b/builder/local_relay.go index 2c782cd364..0a3a73d6f1 100644 --- a/builder/local_relay.go +++ b/builder/local_relay.go @@ -311,17 +311,17 @@ func (r *LocalRelay) handleGetBlock(w http.ResponseWriter, req *http.Request) { bestPayload := r.bestPayload r.bestDataLock.Unlock() + if bestHeader == nil || bestPayload == nil { + respondError(w, http.StatusBadRequest, "no payloads") + return + } + // TODO: [pnowosie] remove or lower severity log.Info("================= Req received ===================", "parent", parentHashHex) header := fmt.Sprintf("Parent : %s\n, BlockNumber : %d\n, BlockHash : %s\n, Timestamp : %d\n", bestHeader.ParentHash, bestHeader.BlockNumber, bestHeader.BlockHash, bestHeader.Timestamp) log.Info("👉 Best Header", "", header) - if bestHeader == nil || bestPayload == nil { - respondError(w, http.StatusBadRequest, "no payloads") - return - } - if bestHeader == nil || bestHeader.ParentHash.String() != parentHashHex { respondError(w, http.StatusBadRequest, "unknown payload") return diff --git a/builder/service.go b/builder/service.go index cb175129ce..756bf4aeef 100644 --- a/builder/service.go +++ b/builder/service.go @@ -28,7 +28,7 @@ const ( _PathRegisterValidator = "/eth/v1/builder/validators" _PathGetHeader = "/eth/v1/builder/header/{slot:[0-9]+}/{parent_hash:0x[a-fA-F0-9]+}/{pubkey:0x[a-fA-F0-9]+}" _PathGetPayload = "/eth/v1/builder/blinded_blocks" - _PathGetBlock = "/eth/v1/builder/block/{parent_hash:0x[a-fA-F0-9]+}" + _PathGetBlock = "/eth/v1/builder/block/{slot:[0-9]+}/{parent_hash:0x[a-fA-F0-9]+}" ) type Service struct { From e933b62ec742e83498d562b1891aaa9c1c6f455d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Nowosielski?= Date: Tue, 21 Nov 2023 15:39:42 +0100 Subject: [PATCH 7/7] Builder: working version --- Makefile | 2 +- builder/local_relay.go | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/Makefile b/Makefile index 717b12e110..aca51eceb6 100644 --- a/Makefile +++ b/Makefile @@ -31,7 +31,7 @@ bor: builder: mkdir -p $(GOPATH)/bin/ - go build -o $(GOBIN)/bbuilder $(GO_LDFLAGS) ./cmd/cli/main.go + go build -o $(GOBIN)/builder $(GO_LDFLAGS) ./cmd/cli/main.go #cp $(GOBIN)/bor $(GOPATH)/bin/ @echo "Done building." diff --git a/builder/local_relay.go b/builder/local_relay.go index 0a3a73d6f1..7e57bf1d68 100644 --- a/builder/local_relay.go +++ b/builder/local_relay.go @@ -316,12 +316,6 @@ func (r *LocalRelay) handleGetBlock(w http.ResponseWriter, req *http.Request) { return } - // TODO: [pnowosie] remove or lower severity - log.Info("================= Req received ===================", "parent", parentHashHex) - header := fmt.Sprintf("Parent : %s\n, BlockNumber : %d\n, BlockHash : %s\n, Timestamp : %d\n", - bestHeader.ParentHash, bestHeader.BlockNumber, bestHeader.BlockHash, bestHeader.Timestamp) - log.Info("👉 Best Header", "", header) - if bestHeader == nil || bestHeader.ParentHash.String() != parentHashHex { respondError(w, http.StatusBadRequest, "unknown payload") return