From ba64fe43d640acb197a5839d936ce6370bb2b6b9 Mon Sep 17 00:00:00 2001 From: Herbert Jordan Date: Fri, 20 Dec 2024 07:43:55 +0100 Subject: [PATCH] Start implementing network rule validation --- gossip/blockproc/drivermodule/driver_txs.go | 5 +- integration/makegenesis/genesis.go | 4 + opera/marshal.go | 18 ++- opera/marshal_test.go | 5 +- opera/rules.go | 15 +- opera/validate.go | 162 ++++++++++++++++++++ opera/validate_test.go | 19 +++ 7 files changed, 213 insertions(+), 15 deletions(-) create mode 100644 opera/validate.go create mode 100644 opera/validate_test.go diff --git a/gossip/blockproc/drivermodule/driver_txs.go b/gossip/blockproc/drivermodule/driver_txs.go index 20f3a2d52..1f9002a7d 100644 --- a/gossip/blockproc/drivermodule/driver_txs.go +++ b/gossip/blockproc/drivermodule/driver_txs.go @@ -23,7 +23,8 @@ import ( ) const ( - maxAdvanceEpochs = 1 << 16 + internalTransactionsGasLimit = opera.MinimumMaxBlockGas / 2 + maxAdvanceEpochs = 1 << 16 ) type DriverTxListenerModule struct{} @@ -66,7 +67,7 @@ func InternalTxBuilder(statedb state.StateDB) func(calldata []byte, addr common. if nonce == math.MaxUint64 { nonce = statedb.GetNonce(common.Address{}) } - tx := types.NewTransaction(nonce, addr, common.Big0, 500_000_000, common.Big0, calldata) + tx := types.NewTransaction(nonce, addr, common.Big0, internalTransactionsGasLimit, common.Big0, calldata) nonce++ return tx } diff --git a/integration/makegenesis/genesis.go b/integration/makegenesis/genesis.go index fcaee3fe9..bd230cf8e 100644 --- a/integration/makegenesis/genesis.go +++ b/integration/makegenesis/genesis.go @@ -169,6 +169,10 @@ func (b *GenesisBuilder) FinalizeBlockZero( return common.Hash{}, common.Hash{}, errors.New("block zero already finalized") } + if err := rules.Validate(); err != nil { + return common.Hash{}, common.Hash{}, fmt.Errorf("invalid rules: %w", err) + } + // construct state root of initial state b.tmpStateDB.EndBlock(0) genesisStateRoot := b.tmpStateDB.GetStateHash() diff --git a/opera/marshal.go b/opera/marshal.go index 7dc6b4bbb..652f01257 100644 --- a/opera/marshal.go +++ b/opera/marshal.go @@ -2,15 +2,19 @@ package opera import "encoding/json" -func UpdateRules(src Rules, diff []byte) (res Rules, err error) { +func UpdateRules(src Rules, diff []byte) (Rules, error) { changed := src.Copy() - err = json.Unmarshal(diff, &changed) + err := json.Unmarshal(diff, &changed) if err != nil { - return src, err + return Rules{}, err } // protect readonly fields - res = changed - res.NetworkID = src.NetworkID - res.Name = src.Name - return + changed.NetworkID = src.NetworkID + changed.Name = src.Name + + // check validity of the new rules + if err = changed.Validate(); err != nil { + return Rules{}, err + } + return changed, nil } diff --git a/opera/marshal_test.go b/opera/marshal_test.go index 5f7dcc6cf..684763338 100644 --- a/opera/marshal_test.go +++ b/opera/marshal_test.go @@ -16,10 +16,11 @@ func TestUpdateRules(t *testing.T) { exp.Dag.MaxParents = 5 exp.Economy.MinGasPrice = big.NewInt(7) + exp.Economy.MinBaseFee = big.NewInt(12) exp.Blocks.MaxBlockGas = 1000 - got, err := UpdateRules(exp, []byte(`{"Dag":{"MaxParents":5},"Economy":{"MinGasPrice":7},"Blocks":{"MaxBlockGas":1000}}`)) + got, err := UpdateRules(exp, []byte(`{"Dag":{"MaxParents":5},"Economy":{"MinGasPrice":7},"Blocks":{"MaxBlockGas":2000000000}}`)) require.NoError(err) - require.Equal(exp.String(), got.String(), "mutate fields") + require.Equal(exp.String(), got.String(), "mutate fields") // < this test checks nothing; todo: fix exp.Dag.MaxParents = 0 got, err = UpdateRules(exp, []byte(`{"Name":"xxx","NetworkID":1,"Dag":{"MaxParents":0}}`)) diff --git a/opera/rules.go b/opera/rules.go index 7220c427d..a315544db 100644 --- a/opera/rules.go +++ b/opera/rules.go @@ -2,6 +2,7 @@ package opera import ( "encoding/json" + "math" "math/big" "time" @@ -26,8 +27,9 @@ const ( llrBit = 1 << 2 sonicBit = 1 << 3 - defaultMaxBlockGas = 1_000_000_000 - defaultTargetGasRate = 15_000_000 // 15 MGas/s + MinimumMaxBlockGas = 1_000_000_000 // < must be large enough to allow internal transactions to seal blocks + MaximumMaxBlockGas = math.MaxInt64 // < should fit into 64-bit signed integers to avoid parsing errors in third-party libraries + defaultTargetGasRate = 15_000_000 // 15 MGas/s defaultEventEmitterInterval = 600 * time.Millisecond ) @@ -297,7 +299,7 @@ func MainNetRules() Rules { Epochs: DefaultEpochsRules(), Economy: DefaultEconomyRules(), Blocks: BlocksRules{ - MaxBlockGas: defaultMaxBlockGas, + MaxBlockGas: MinimumMaxBlockGas, MaxEmptyBlockSkipPeriod: inter.Timestamp(1 * time.Minute), }, } @@ -312,7 +314,7 @@ func FakeNetRules() Rules { Epochs: FakeNetEpochsRules(), Economy: FakeEconomyRules(), Blocks: BlocksRules{ - MaxBlockGas: defaultMaxBlockGas, + MaxBlockGas: MinimumMaxBlockGas, MaxEmptyBlockSkipPeriod: inter.Timestamp(3 * time.Second), }, Upgrades: Upgrades{ @@ -414,9 +416,14 @@ func DefaultGasPowerRules() GasPowerRules { func (r Rules) Copy() Rules { cp := r cp.Economy.MinGasPrice = new(big.Int).Set(r.Economy.MinGasPrice) + cp.Economy.MinBaseFee = new(big.Int).Set(r.Economy.MinBaseFee) return cp } +func (r Rules) Validate() error { + return validate(r) +} + func (r Rules) String() string { b, _ := json.Marshal(&r) return string(b) diff --git a/opera/validate.go b/opera/validate.go new file mode 100644 index 000000000..d104fcc17 --- /dev/null +++ b/opera/validate.go @@ -0,0 +1,162 @@ +package opera + +import ( + "errors" + "math/big" + "time" + + "github.com/Fantom-foundation/go-opera/inter" +) + +var ( + maxMinimumGasPrice = new(big.Int).SetUint64(1000 * 1e9) // 1000 Gwei +) + +func validate(rules Rules) error { + return errors.Join( + validateDagRules(rules.Dag), + validateEmitterRules(rules.Emitter), + validateEpochsRules(rules.Epochs), + validateBlockRules(rules.Blocks), + validateEconomyRules(rules.Economy), + validateUpgrades(rules.Upgrades), + ) +} + +func validateDagRules(rules DagRules) error { + var issues []error + + if rules.MaxParents < 2 { + issues = append(issues, errors.New("Dag.MaxParents is too low")) + } + + if rules.MaxExtraData > 1<<20 { // 1 MB + issues = append(issues, errors.New("Dag.MaxExtraData is too high")) + } + + return errors.Join(issues...) +} + +func validateEmitterRules(rules EmitterRules) error { + + var issues []error + if rules.Interval < inter.Timestamp(100*time.Millisecond) { + issues = append(issues, errors.New("Emitter.Interval is too low")) + } + if rules.Interval > inter.Timestamp(10*time.Second) { + issues = append(issues, errors.New("Emitter.Interval is too high")) + } + + if rules.StallThreshold < inter.Timestamp(10*time.Second) { + issues = append(issues, errors.New("Emitter.StallThreshold is too low")) + } + + if rules.StalledInterval < inter.Timestamp(10*time.Second) { + issues = append(issues, errors.New("Emitter.StalledInterval is too low")) + } + if rules.StalledInterval > inter.Timestamp(1*time.Minute) { + issues = append(issues, errors.New("Emitter.StalledInterval is too high")) + } + + return errors.Join(issues...) +} + +func validateEpochsRules(rules EpochsRules) error { + var issues []error + + // MaxEpochGas is not restricted. If it is too low, we will have an epoch per block, which is + // not great performance-wise, but it is not invalid. If it is too high, the time limit will + // eventually end a long epoch. + + if rules.MaxEpochDuration > inter.Timestamp(24*time.Hour) { + issues = append(issues, errors.New("Epochs.MaxEpochDuration is too high")) + } + + return errors.Join(issues...) +} + +func validateBlockRules(rules BlocksRules) error { + var issues []error + + if rules.MaxBlockGas < MinimumMaxBlockGas { + issues = append(issues, errors.New("MaxBlockGas is too low")) + } + if rules.MaxBlockGas > MaximumMaxBlockGas { + issues = append(issues, errors.New("MaxBlockGas is too high")) + } + + // The empty-block skip period is not restricted. There are no too low or too high values. + + return errors.Join(issues...) +} + +func validateEconomyRules(rules EconomyRules) error { + var issues []error + + if rules.MinGasPrice == nil { + issues = append(issues, errors.New("MinGasPrice is nil")) + } else { + if rules.MinGasPrice.Sign() < 0 { + issues = append(issues, errors.New("MinGasPrice is negative")) + } + if rules.MinGasPrice.Cmp(maxMinimumGasPrice) > 0 { + issues = append(issues, errors.New("MinGasPrice is too high")) + } + } + + if rules.MinBaseFee == nil { + issues = append(issues, errors.New("MinBaseFee is nil")) + } else { + if rules.MinBaseFee.Sign() < 0 { + issues = append(issues, errors.New("MinBaseFee is negative")) + } + if rules.MinBaseFee.Cmp(maxMinimumGasPrice) > 0 { + issues = append(issues, errors.New("MinBaseFee is too high")) + } + } + + // TODO: check BlockMissedSlack + + issues = append(issues, validateGasRules(rules.Gas)) + issues = append(issues, validateGasPowerRules("Economy.ShortGasPower", rules.ShortGasPower)) + issues = append(issues, validateGasPowerRules("Economy.LongGasPower", rules.LongGasPower)) + + return errors.Join(issues...) +} + +func validateGasRules(rules GasRules) error { + var issues []error + + // TODO: implement + + return errors.Join(issues...) +} + +func validateGasPowerRules(prefix string, rules GasPowerRules) error { + var issues []error + + // TODO: implement + + return errors.Join(issues...) +} + +func validateUpgrades(upgrade Upgrades) error { + var issues []error + + if upgrade.Llr { + issues = append(issues, errors.New("LLR upgrade is not supported")) + } + + if upgrade.Sonic && !upgrade.London { + issues = append(issues, errors.New("Sonic upgrade requires London")) + } + if upgrade.London && !upgrade.Berlin { + issues = append(issues, errors.New("London upgrade requires Berlin")) + } + + if !upgrade.Sonic { + issues = append(issues, errors.New("Sonic upgrade is required")) + } + + return errors.Join(issues...) +} diff --git a/opera/validate_test.go b/opera/validate_test.go new file mode 100644 index 000000000..fa6ba0502 --- /dev/null +++ b/opera/validate_test.go @@ -0,0 +1,19 @@ +package opera + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestDefaultRulesAreValid(t *testing.T) { + rules := map[string]Rules{ + "mainnet": MainNetRules(), + "fakenet": FakeNetRules(), + } + for name, r := range rules { + t.Run(name, func(t *testing.T) { + require.NoError(t, r.Validate()) + }) + } +}