diff --git a/cmd/opera/launcher/config.go b/cmd/opera/launcher/config.go index bc343723d..92fbd57dd 100644 --- a/cmd/opera/launcher/config.go +++ b/cmd/opera/launcher/config.go @@ -4,6 +4,7 @@ import ( "bufio" "errors" "fmt" + "math/big" "os" "path" "path/filepath" @@ -545,6 +546,13 @@ func mayMakeAllConfigs(ctx *cli.Context) (*config, error) { // Process DBs defaults in the end because they are applied only in absence of config or flags cfg = setDBConfigDefault(cfg, cacheRatio) + // Sanitize GPO config + if cfg.Opera.GPO.MinGasTip == nil || cfg.Opera.GPO.MinGasTip.Sign() == 0 { + cfg.Opera.GPO.MinGasTip = new(big.Int).SetUint64(cfg.TxPool.PriceLimit) + } + if cfg.Opera.GPO.MinGasTip.Cmp(new(big.Int).SetUint64(cfg.TxPool.PriceLimit)) < 0 { + log.Warn(fmt.Sprintf("GPO minimum gas tip (Opera.GPO.MinGasTip=%s) is lower than txpool minimum gas tip (TxPool.PriceLimit=%d)", cfg.Opera.GPO.MinGasTip.String(), cfg.TxPool.PriceLimit)) + } if err := cfg.Opera.Validate(); err != nil { return nil, err diff --git a/ethapi/api.go b/ethapi/api.go index 4919f3ccf..3149c6700 100644 --- a/ethapi/api.go +++ b/ethapi/api.go @@ -21,6 +21,7 @@ import ( "errors" "fmt" "math/big" + "math/rand" "runtime" "strings" "sync" @@ -99,6 +100,7 @@ type feeHistoryResult struct { Reward [][]*hexutil.Big `json:"reward,omitempty"` BaseFee []*hexutil.Big `json:"baseFeePerGas,omitempty"` GasUsedRatio []float64 `json:"gasUsedRatio"` + Note string `json:"note"` } var errInvalidPercentile = errors.New("invalid reward percentile") @@ -138,17 +140,35 @@ func (s *PublicEthereumAPI) FeeHistory(ctx context.Context, blockCount rpc.Decim baseFee := s.b.MinGasPrice() - tips := make([]*hexutil.Big, 0, len(rewardPercentiles)) + tips := make([]*big.Int, 0, len(rewardPercentiles)) for _, p := range rewardPercentiles { tip := s.b.SuggestGasTipCap(ctx, uint64(gasprice.DecimalUnit*p/100.0)) - tips = append(tips, (*hexutil.Big)(tip)) + tips = append(tips, tip) } res.OldestBlock.ToInt().SetUint64(uint64(oldest)) for i := uint64(0); i < uint64(last-oldest+1); i++ { - res.Reward = append(res.Reward, tips) + // randomize the output to mimic the ETH API eth_feeHistory for compatibility reasons + rTips := make([]*hexutil.Big, 0, len(tips)) + for _, t := range tips { + rTip := t + // don't randomize last iteration + if i < uint64(last-oldest) { + // increase by up to 2% randomly + rTip = new(big.Int).Mul(t, big.NewInt(int64(rand.Intn(gasprice.DecimalUnit/50)+gasprice.DecimalUnit))) + rTip.Div(rTip, big.NewInt(gasprice.DecimalUnit)) + } + rTips = append(rTips, (*hexutil.Big)(rTip)) + } + res.Reward = append(res.Reward, rTips) res.BaseFee = append(res.BaseFee, (*hexutil.Big)(baseFee)) - res.GasUsedRatio = append(res.GasUsedRatio, 0.99) - } + r := rand.New(rand.NewSource(int64(oldest) + int64(i))) + res.GasUsedRatio = append(res.GasUsedRatio, 0.9+r.Float64()*0.1) + } + res.Note = `In the FTM network, the eth_feeHistory method operates slightly differently due to the network's unique consensus mechanism. ` + + `Here, instead of returning a range of gas tip values from requested blocks, ` + + `it provides a singular estimated gas tip based on a defined confidence level (indicated by the percentile parameter). ` + + `This approach means that while you will receive replicated (and randomized) reward values across the requested blocks, ` + + `the average or median of these values remains consistent with the intended gas tip.` return res, nil } diff --git a/evmcore/tx_pool.go b/evmcore/tx_pool.go index 754d4cde7..f994fb626 100644 --- a/evmcore/tx_pool.go +++ b/evmcore/tx_pool.go @@ -153,7 +153,7 @@ type TxPoolConfig struct { Journal string // Journal of local transactions to survive node restarts Rejournal time.Duration // Time interval to regenerate the local transaction journal - PriceLimit uint64 // Minimum gas price to enforce for acceptance into the pool + PriceLimit uint64 // Minimum gas tip to enforce for acceptance into the pool PriceBump uint64 // Minimum price bump percentage to replace an already existing transaction (nonce) AccountSlots uint64 // Number of executable transaction slots guaranteed per account @@ -170,7 +170,7 @@ var DefaultTxPoolConfig = TxPoolConfig{ Journal: "transactions.rlp", Rejournal: time.Hour, - PriceLimit: 1, + PriceLimit: 0, PriceBump: 10, AccountSlots: 16, @@ -189,10 +189,6 @@ func (config *TxPoolConfig) sanitize() TxPoolConfig { log.Warn("Sanitizing invalid txpool journal time", "provided", conf.Rejournal, "updated", time.Second) conf.Rejournal = time.Second } - if conf.PriceLimit < 1 { - log.Warn("Sanitizing invalid txpool price limit", "provided", conf.PriceLimit, "updated", DefaultTxPoolConfig.PriceLimit) - conf.PriceLimit = DefaultTxPoolConfig.PriceLimit - } if conf.PriceBump < 1 { log.Warn("Sanitizing invalid txpool price bump", "provided", conf.PriceBump, "updated", DefaultTxPoolConfig.PriceBump) conf.PriceBump = DefaultTxPoolConfig.PriceBump diff --git a/gossip/config.go b/gossip/config.go index 8cde7a890..67b9562da 100644 --- a/gossip/config.go +++ b/gossip/config.go @@ -214,6 +214,7 @@ func DefaultConfig(scale cachescale.Func) Config { GPO: gasprice.Config{ MaxGasPrice: gasprice.DefaultMaxGasPrice, MinGasPrice: new(big.Int), + MinGasTip: new(big.Int), DefaultCertainty: 0.5 * gasprice.DecimalUnit, }, diff --git a/gossip/emitter/emitter_llr.go b/gossip/emitter/emitter_llr.go index 26c169846..a65ce6ed1 100644 --- a/gossip/emitter/emitter_llr.go +++ b/gossip/emitter/emitter_llr.go @@ -29,22 +29,26 @@ func (em *Emitter) addLlrBlockVotes(e *inter.MutableEventPayload) { if prevInFile != nil && start < *prevInFile+1 { start = *prevInFile + 1 } - records := make([]hash.Hash, 0, 16) + records := make([]hash.Hash, 0, basiccheck.MaxBlockVotesPerEvent) epochEnd := false var epoch idx.Epoch for b := start; len(records) < basiccheck.MaxBlockVotesPerEvent; b++ { - record := em.world.GetBlockRecordHash(b) - if record == nil { - break - } blockEpoch := em.world.GetBlockEpoch(b) if epoch == 0 { + if !em.isEpochValidator(blockEpoch) { + continue + } epoch = blockEpoch + start = b } if epoch != blockEpoch || blockEpoch == 0 { epochEnd = true break } + record := em.world.GetBlockRecordHash(b) + if record == nil { + break + } records = append(records, *record) } @@ -73,6 +77,9 @@ func (em *Emitter) addLlrEpochVote(e *inter.MutableEventPayload) { if prevInFile != nil && target < *prevInFile+1 { target = *prevInFile + 1 } + if !em.isEpochValidator(target) { + return + } vote := em.world.GetEpochRecordHash(target) if vote == nil { return @@ -106,3 +113,13 @@ func (em *Emitter) skipLlrEpochVote() bool { // otherwise, poor validators have a small chance to vote return rand.Intn(30) != 0 } + +func (em *Emitter) isEpochValidator(epoch idx.Epoch) bool { + es := em.world.GetHistoryEpochState(epoch) + if es == nil { + return false + } + + _, ok := es.ValidatorProfiles[em.config.Validator.ID] + return ok +} diff --git a/gossip/emitter/mock/world.go b/gossip/emitter/mock/world.go index 97c2f59ee..6c51c8573 100644 --- a/gossip/emitter/mock/world.go +++ b/gossip/emitter/mock/world.go @@ -9,6 +9,7 @@ import ( reflect "reflect" inter "github.com/Fantom-foundation/go-opera/inter" + iblockproc "github.com/Fantom-foundation/go-opera/inter/iblockproc" validatorpk "github.com/Fantom-foundation/go-opera/inter/validatorpk" opera "github.com/Fantom-foundation/go-opera/opera" vecmt "github.com/Fantom-foundation/go-opera/vecmt" @@ -211,6 +212,20 @@ func (mr *MockExternalMockRecorder) GetHeads(arg0 interface{}) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetHeads", reflect.TypeOf((*MockExternal)(nil).GetHeads), arg0) } +// GetHistoryEpochState mocks base method. +func (m *MockExternal) GetHistoryEpochState(arg0 idx.Epoch) *iblockproc.EpochState { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetHistoryEpochState", arg0) + ret0, _ := ret[0].(*iblockproc.EpochState) + return ret0 +} + +// GetHistoryEpochState indicates an expected call of GetHistoryEpochState. +func (mr *MockExternalMockRecorder) GetHistoryEpochState(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetHistoryEpochState", reflect.TypeOf((*MockExternal)(nil).GetHistoryEpochState), arg0) +} + // GetLastBV mocks base method. func (m *MockExternal) GetLastBV(arg0 idx.ValidatorID) *idx.Block { m.ctrl.T.Helper() diff --git a/gossip/emitter/world.go b/gossip/emitter/world.go index aeea9d31a..73a7d95e8 100644 --- a/gossip/emitter/world.go +++ b/gossip/emitter/world.go @@ -12,6 +12,7 @@ import ( "github.com/ethereum/go-ethereum/core/types" "github.com/Fantom-foundation/go-opera/inter" + "github.com/Fantom-foundation/go-opera/inter/iblockproc" "github.com/Fantom-foundation/go-opera/opera" "github.com/Fantom-foundation/go-opera/valkeystore" "github.com/Fantom-foundation/go-opera/vecmt" @@ -69,6 +70,7 @@ type Reader interface { LlrReader GetLatestBlockIndex() idx.Block GetEpochValidators() (*pos.Validators, idx.Epoch) + GetHistoryEpochState(epoch idx.Epoch) *iblockproc.EpochState GetEvent(hash.Event) *inter.Event GetEventPayload(hash.Event) *inter.EventPayload GetLastEvent(epoch idx.Epoch, from idx.ValidatorID) *hash.Event diff --git a/gossip/gasprice/gasprice.go b/gossip/gasprice/gasprice.go index 64997da2f..4035a0bdf 100644 --- a/gossip/gasprice/gasprice.go +++ b/gossip/gasprice/gasprice.go @@ -49,6 +49,7 @@ const ( type Config struct { MaxGasPrice *big.Int `toml:",omitempty"` MinGasPrice *big.Int `toml:",omitempty"` + MinGasTip *big.Int `toml:",omitempty"` DefaultCertainty uint64 `toml:",omitempty"` } @@ -107,7 +108,8 @@ func sanitizeBigInt(val, min, max, _default *big.Int, name string) *big.Int { // gasprice for newly created transaction. func NewOracle(params Config) *Oracle { params.MaxGasPrice = sanitizeBigInt(params.MaxGasPrice, nil, nil, DefaultMaxGasPrice, "MaxGasPrice") - params.MinGasPrice = sanitizeBigInt(params.MinGasPrice, nil, nil, new(big.Int), "MinGasPrice") + params.MinGasPrice = sanitizeBigInt(params.MinGasPrice, nil, params.MaxGasPrice, new(big.Int), "MinGasPrice") + params.MinGasTip = sanitizeBigInt(params.MinGasTip, nil, new(big.Int).Sub(params.MaxGasPrice, params.MinGasPrice), new(big.Int), "MinGasTip") params.DefaultCertainty = sanitizeBigInt(new(big.Int).SetUint64(params.DefaultCertainty), big.NewInt(0), DecimalUnitBn, big.NewInt(DecimalUnit/2), "DefaultCertainty").Uint64() tCache, _ := lru.New(100) return &Oracle{ @@ -148,8 +150,8 @@ func (gpo *Oracle) suggestTip(certainty uint64) *big.Int { } tip := new(big.Int).Sub(combined, minPrice) - if tip.Sign() < 0 { - return new(big.Int) + if tip.Cmp(gpo.cfg.MinGasTip) < 0 { + return new(big.Int).Set(gpo.cfg.MinGasTip) } return tip } diff --git a/version/version.go b/version/version.go index eb8670140..0370454ee 100644 --- a/version/version.go +++ b/version/version.go @@ -8,10 +8,10 @@ import ( ) func init() { - params.VersionMajor = 1 // Major version component of the current release - params.VersionMinor = 1 // Minor version component of the current release - params.VersionPatch = 3 // Patch version component of the current release - params.VersionMeta = "txtracing-rc.3.1" // Version metadata to append to the version string + params.VersionMajor = 1 // Major version component of the current release + params.VersionMinor = 1 // Minor version component of the current release + params.VersionPatch = 3 // Patch version component of the current release + params.VersionMeta = "txtracing-rc.4" // Version metadata to append to the version string } func BigToString(b *big.Int) string {