diff --git a/command/ibft/ibft.go b/command/ibft/ibft.go index a1fa116c5a..13b78ba0c8 100644 --- a/command/ibft/ibft.go +++ b/command/ibft/ibft.go @@ -4,6 +4,7 @@ import ( "github.com/0xPolygon/polygon-edge/command/helper" "github.com/0xPolygon/polygon-edge/command/ibft/candidates" "github.com/0xPolygon/polygon-edge/command/ibft/propose" + "github.com/0xPolygon/polygon-edge/command/ibft/quorum" "github.com/0xPolygon/polygon-edge/command/ibft/snapshot" "github.com/0xPolygon/polygon-edge/command/ibft/status" _switch "github.com/0xPolygon/polygon-edge/command/ibft/switch" @@ -35,5 +36,7 @@ func registerSubcommands(baseCmd *cobra.Command) { candidates.GetCommand(), // ibft switch _switch.GetCommand(), + // ibft quorum + quorum.GetCommand(), ) } diff --git a/command/ibft/quorum/ibft_quorum.go b/command/ibft/quorum/ibft_quorum.go new file mode 100644 index 0000000000..1699dc87c0 --- /dev/null +++ b/command/ibft/quorum/ibft_quorum.go @@ -0,0 +1,66 @@ +package quorum + +import ( + "fmt" + "github.com/0xPolygon/polygon-edge/command" + "github.com/spf13/cobra" +) + +func GetCommand() *cobra.Command { + ibftQuorumCmd := &cobra.Command{ + Use: "quorum", + Short: "Specify the block number after which quorum optimal will be used for reaching consensus", + PreRunE: runPreRun, + Run: runCommand, + } + + setFlags(ibftQuorumCmd) + setRequiredFlags(ibftQuorumCmd) + + return ibftQuorumCmd +} + +func setFlags(cmd *cobra.Command) { + cmd.Flags().StringVar( + ¶ms.genesisPath, + chainFlag, + fmt.Sprintf("./%s", command.DefaultGenesisFileName), + "the genesis file to update", + ) + + cmd.Flags().Uint64Var( + ¶ms.from, + fromFlag, + 0, + "the height to switch the quorum calculation", + ) +} + +func setRequiredFlags(cmd *cobra.Command) { + for _, requiredFlag := range params.getRequiredFlags() { + _ = cmd.MarkFlagRequired(requiredFlag) + } +} + +func runPreRun(_ *cobra.Command, _ []string) error { + return params.initRawParams() +} + +func runCommand(cmd *cobra.Command, _ []string) { + outputter := command.InitializeOutputter(cmd) + defer outputter.WriteOutput() + + if err := params.updateGenesisConfig(); err != nil { + outputter.SetError(err) + + return + } + + if err := params.overrideGenesisConfig(); err != nil { + outputter.SetError(err) + + return + } + + outputter.SetCommandResult(params.getResult()) +} diff --git a/command/ibft/quorum/params.go b/command/ibft/quorum/params.go new file mode 100644 index 0000000000..9ddc270b84 --- /dev/null +++ b/command/ibft/quorum/params.go @@ -0,0 +1,98 @@ +package quorum + +import ( + "errors" + "fmt" + "github.com/0xPolygon/polygon-edge/chain" + "github.com/0xPolygon/polygon-edge/command" + "github.com/0xPolygon/polygon-edge/command/helper" + "github.com/0xPolygon/polygon-edge/helper/common" + "os" +) + +const ( + fromFlag = "from" + chainFlag = "chain" +) + +var ( + params = &quorumParams{} +) + +type quorumParams struct { + genesisConfig *chain.Chain + from uint64 + genesisPath string +} + +func (p *quorumParams) initChain() error { + cc, err := chain.Import(p.genesisPath) + if err != nil { + return fmt.Errorf( + "failed to load chain config from %s: %w", + p.genesisPath, + err, + ) + } + + p.genesisConfig = cc + + return nil +} + +func (p *quorumParams) initRawParams() error { + return p.initChain() +} + +func (p *quorumParams) getRequiredFlags() []string { + return []string{ + fromFlag, + } +} + +func (p *quorumParams) updateGenesisConfig() error { + return appendIBFTQuorum( + p.genesisConfig, + p.from, + ) +} + +func (p *quorumParams) overrideGenesisConfig() error { + // Remove the current genesis configuration from disk + if err := os.Remove(p.genesisPath); err != nil { + return err + } + + // Save the new genesis configuration + if err := helper.WriteGenesisConfigToDisk( + p.genesisConfig, + p.genesisPath, + ); err != nil { + return err + } + + return nil +} + +func (p *quorumParams) getResult() command.CommandResult { + return &IBFTQuorumResult{ + Chain: p.genesisPath, + From: common.JSONNumber{Value: p.from}, + } +} + +func appendIBFTQuorum( + cc *chain.Chain, + from uint64, +) error { + ibftConfig, ok := cc.Params.Engine["ibft"].(map[string]interface{}) + if !ok { + return errors.New(`"ibft" setting doesn't exist in "engine" of genesis.json'`) + } + + ibftConfig["quorumSizeBlockNum"] = from + + cc.Params.Engine["ibft"] = ibftConfig + + return nil +} diff --git a/command/ibft/quorum/result.go b/command/ibft/quorum/result.go new file mode 100644 index 0000000000..30ba750da4 --- /dev/null +++ b/command/ibft/quorum/result.go @@ -0,0 +1,29 @@ +package quorum + +import ( + "bytes" + "fmt" + "github.com/0xPolygon/polygon-edge/command/helper" + "github.com/0xPolygon/polygon-edge/helper/common" +) + +type IBFTQuorumResult struct { + Chain string `json:"chain"` + From common.JSONNumber `json:"from"` +} + +func (r *IBFTQuorumResult) GetOutput() string { + var buffer bytes.Buffer + + buffer.WriteString("\n[NEW IBFT QUORUM START]\n") + + outputs := []string{ + fmt.Sprintf("Chain|%s", r.Chain), + fmt.Sprintf("From|%d", r.From.Value), + } + + buffer.WriteString(helper.FormatKV(outputs)) + buffer.WriteString("\n") + + return buffer.String() +} diff --git a/consensus/ibft/ibft.go b/consensus/ibft/ibft.go index 7113b15efa..c1a9a0c842 100644 --- a/consensus/ibft/ibft.go +++ b/consensus/ibft/ibft.go @@ -78,8 +78,9 @@ type Ibft struct { txpool txPoolInterface // Reference to the transaction pool - store *snapshotStore // Snapshot store that keeps track of all snapshots - epochSize uint64 + store *snapshotStore // Snapshot store that keeps track of all snapshots + epochSize uint64 + quorumSizeBlockNum uint64 msgQueue *msgQueue // Structure containing different message queues updateCh chan struct{} // Update channel @@ -133,11 +134,13 @@ func (i *Ibft) runHook(hookName HookType, height uint64, hookParam interface{}) func Factory( params *consensus.ConsensusParams, ) (consensus.Consensus, error) { - var epochSize uint64 - if definedEpochSize, ok := params.Config.Config["epochSize"]; !ok { - // No epoch size defined, use the default one - epochSize = DefaultEpochSize - } else { + // defaults for user set fields in genesis + var ( + epochSize = uint64(DefaultEpochSize) + quorumSizeBlockNum = uint64(0) + ) + + if definedEpochSize, ok := params.Config.Config["epochSize"]; ok { // Epoch size is defined, use the passed in one readSize, ok := definedEpochSize.(float64) if !ok { @@ -147,21 +150,32 @@ func Factory( epochSize = uint64(readSize) } + if rawBlockNum, ok := params.Config.Config["quorumSizeBlockNum"]; ok { + // Block number specified for quorum size switch + readBlockNum, ok := rawBlockNum.(float64) + if !ok { + return nil, errors.New("invalid type assertion") + } + + quorumSizeBlockNum = uint64(readBlockNum) + } + p := &Ibft{ - logger: params.Logger.Named("ibft"), - config: params.Config, - Grpc: params.Grpc, - blockchain: params.Blockchain, - executor: params.Executor, - closeCh: make(chan struct{}), - txpool: params.Txpool, - state: ¤tState{}, - network: params.Network, - epochSize: epochSize, - sealing: params.Seal, - metrics: params.Metrics, - secretsManager: params.SecretsManager, - blockTime: time.Duration(params.BlockTime) * time.Second, + logger: params.Logger.Named("ibft"), + config: params.Config, + Grpc: params.Grpc, + blockchain: params.Blockchain, + executor: params.Executor, + closeCh: make(chan struct{}), + txpool: params.Txpool, + state: ¤tState{}, + network: params.Network, + epochSize: epochSize, + quorumSizeBlockNum: quorumSizeBlockNum, + sealing: params.Seal, + metrics: params.Metrics, + secretsManager: params.SecretsManager, + blockTime: time.Duration(params.BlockTime) * time.Second, } // Initialize the mechanism @@ -916,12 +930,12 @@ func (i *Ibft) runValidateState() { panic(fmt.Sprintf("BUG: %s", reflect.TypeOf(msg.Type))) } - if i.state.numPrepared() >= i.state.validators.QuorumSize() { + if i.state.numPrepared() >= i.quorumSize(i.state.view.Sequence)(i.state.validators) { // we have received enough pre-prepare messages sendCommit() } - if i.state.numCommitted() >= i.state.validators.QuorumSize() { + if i.state.numCommitted() >= i.quorumSize(i.state.view.Sequence)(i.state.validators) { // we have received enough commit messages sendCommit() @@ -1107,7 +1121,7 @@ func (i *Ibft) runRoundChangeState() { // update timer timeout = exponentialTimeout(i.state.view.Round) sendRoundChange(msg.View.Round) - } else if num == i.state.validators.QuorumSize() { + } else if num == i.quorumSize(i.state.view.Sequence)(i.state.validators) { // start a new round immediately i.state.view.Round = msg.View.Round i.setState(AcceptState) @@ -1249,13 +1263,24 @@ func (i *Ibft) VerifyHeader(parent, header *types.Header) error { } // verify the committed seals - if err := verifyCommitedFields(snap, header); err != nil { + if err := verifyCommitedFields(snap, header, i.quorumSize(header.Number)); err != nil { return err } return nil } +// quorumSize returns a callback that when executed on a ValidatorSet computes +// number of votes required to reach quorum based on the size of the set. +// The blockNumber argument indicates which formula was used to calculate the result (see PRs #513, #549) +func (i *Ibft) quorumSize(blockNumber uint64) QuorumImplementation { + if blockNumber < i.quorumSizeBlockNum { + return LegacyQuorumSize + } + + return OptimalQuorumSize +} + // ProcessHeaders updates the snapshot based on previously verified headers func (i *Ibft) ProcessHeaders(headers []*types.Header) error { return i.processHeaders(headers) diff --git a/consensus/ibft/ibft_test.go b/consensus/ibft/ibft_test.go index f2e953d75d..65ef8ebffd 100644 --- a/consensus/ibft/ibft_test.go +++ b/consensus/ibft/ibft_test.go @@ -1297,3 +1297,60 @@ func TestGetIBFTForks(t *testing.T) { }) } } + +func TestQuorumSizeSwitch(t *testing.T) { + t.Parallel() + + testTable := []struct { + name string + switchBlock uint64 + currentBlock uint64 + set ValidatorSet + expectedQuorum int + }{ + { + "use old quorum calculation", + 10, + 5, + []types.Address{ + types.ZeroAddress, + types.ZeroAddress, + types.ZeroAddress, + types.ZeroAddress, + types.ZeroAddress, + types.ZeroAddress, + }, + 3, + }, + { + "use new quorum calculation", + 10, + 15, + []types.Address{ + types.ZeroAddress, + types.ZeroAddress, + types.ZeroAddress, + types.ZeroAddress, + types.ZeroAddress, + types.ZeroAddress, + }, + 4, + }, + } + + for _, test := range testTable { + test := test + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + ibft := &Ibft{ + quorumSizeBlockNum: test.switchBlock, + } + + assert.Equal(t, + test.expectedQuorum, + ibft.quorumSize(test.currentBlock)(test.set), + ) + }) + } +} diff --git a/consensus/ibft/sign.go b/consensus/ibft/sign.go index cb2e922b67..258ae01fad 100644 --- a/consensus/ibft/sign.go +++ b/consensus/ibft/sign.go @@ -166,7 +166,11 @@ func verifySigner(snap *Snapshot, header *types.Header) error { } // verifyCommitedFields is checking for consensus proof in the header -func verifyCommitedFields(snap *Snapshot, header *types.Header) error { +func verifyCommitedFields( + snap *Snapshot, + header *types.Header, + quorumSizeFn QuorumImplementation, +) error { extra, err := getIbftExtra(header) if err != nil { return err @@ -207,7 +211,7 @@ func verifyCommitedFields(snap *Snapshot, header *types.Header) error { // Valid committed seals must be at least 2F+1 // 2F is the required number of honest validators who provided the committed seals // +1 is the proposer - if validSeals := len(visited); validSeals < snap.Set.QuorumSize() { + if validSeals := len(visited); validSeals < quorumSizeFn(snap.Set) { return fmt.Errorf("not enough seals to seal block") } diff --git a/consensus/ibft/sign_test.go b/consensus/ibft/sign_test.go index 059b19a437..6f177cb0cb 100644 --- a/consensus/ibft/sign_test.go +++ b/consensus/ibft/sign_test.go @@ -59,7 +59,7 @@ func TestSign_CommittedSeals(t *testing.T) { assert.NoError(t, err) - return verifyCommitedFields(snap, sealed) + return verifyCommitedFields(snap, sealed, OptimalQuorumSize) } // Correct diff --git a/consensus/ibft/state.go b/consensus/ibft/state.go index ba38ad52e2..ef35cb3dbd 100644 --- a/consensus/ibft/state.go +++ b/consensus/ibft/state.go @@ -304,19 +304,28 @@ func (v *ValidatorSet) MaxFaultyNodes() int { return (len(*v) - 1) / 3 } -// QuorumSize returns the number of required messages for consensus -func (v ValidatorSet) QuorumSize() int { +type QuorumImplementation func(ValidatorSet) int + +// LegacyQuorumSize returns the legacy quorum size for the given validator set +func LegacyQuorumSize(set ValidatorSet) int { + // According to the IBFT spec, the number of valid messages + // needs to be 2F + 1 + return 2*set.MaxFaultyNodes() + 1 +} + +// OptimalQuorumSize returns the optimal quorum size for the given validator set +func OptimalQuorumSize(set ValidatorSet) int { // if the number of validators is less than 4, // then the entire set is required - if v.MaxFaultyNodes() == 0 { + if set.MaxFaultyNodes() == 0 { /* N: 1 -> Q: 1 N: 2 -> Q: 2 N: 3 -> Q: 3 */ - return v.Len() + return set.Len() } // (quorum optimal) Q = ceil(2/3 * N) - return int(math.Ceil(2 * float64(v.Len()) / 3)) + return int(math.Ceil(2 * float64(set.Len()) / 3)) } diff --git a/consensus/ibft/state_test.go b/consensus/ibft/state_test.go index 414bc1efbc..5b84fe9eab 100644 --- a/consensus/ibft/state_test.go +++ b/consensus/ibft/state_test.go @@ -62,7 +62,7 @@ func TestNumValid(t *testing.T) { assert.Equal(t, int(c.Quorum), - pool.ValidatorSet().QuorumSize(), + OptimalQuorumSize(pool.ValidatorSet()), ) } }