From 19434b8de6c8613be1ef1a5a660d1098cfaa6610 Mon Sep 17 00:00:00 2001 From: egonspace Date: Tue, 14 May 2024 11:44:48 +0900 Subject: [PATCH] feat: brioche first implementation --- consensus/ethash/consensus.go | 6 +-- core/block_validator.go | 10 +++++ core/blockchain.go | 24 +++--------- core/blockchain_test.go | 34 ++++++++++++++++- core/state_processor.go | 8 ++-- core/types.go | 5 ++- eth/downloader/downloader.go | 1 + eth/state_accessor.go | 2 +- go.mod | 4 +- params/config.go | 41 ++++++++++++++++++--- wemix/admin.go | 69 +++++++++++++++++++++++++++-------- wemix/admin_test.go | 48 ++++++++++++++++++++++++ wemix/miner/miner.go | 6 +-- wemix/rewards_test.go | 59 ++++++++++++++++++++++++++++-- 14 files changed, 259 insertions(+), 58 deletions(-) create mode 100644 wemix/admin_test.go diff --git a/consensus/ethash/consensus.go b/consensus/ethash/consensus.go index 506bd8cb8114..4ab4413b8443 100644 --- a/consensus/ethash/consensus.go +++ b/consensus/ethash/consensus.go @@ -42,7 +42,6 @@ import ( var ( FrontierBlockReward = big.NewInt(5e+18) // Block reward in wei for successfully mining a block ByzantiumBlockReward = big.NewInt(3e+18) // Block reward in wei for successfully mining a block upward from Byzantium - WemixBlockReward = big.NewInt(0) // Block reward in wei for Wemix ConstantinopleBlockReward = big.NewInt(2e+18) // Block reward in wei for successfully mining a block upward from Constantinople maxUncles = 2 // Maximum number of uncles allowed in a single block allowedFutureBlockTimeSeconds = int64(15) // Max seconds from current time allowed for blocks, before they're considered future blocks @@ -698,9 +697,8 @@ func accumulateRewards(config *params.ChainConfig, state *state.StateDB, header } state.AddBalance(header.Coinbase, reward) } else { - blockReward = WemixBlockReward coinbase, rewards, err := wemixminer.CalculateRewards( - header.Number, blockReward, header.Fees, + config, header.Number, header.Fees, func(addr common.Address, amt *big.Int) { state.AddBalance(addr, amt) }) @@ -713,7 +711,7 @@ func accumulateRewards(config *params.ChainConfig, state *state.StateDB, header if err == wemixminer.ErrNotInitialized { reward := new(big.Int) if header.Fees != nil { - reward.Add(blockReward, header.Fees) + reward.Add(reward, header.Fees) } state.AddBalance(header.Coinbase, reward) return nil diff --git a/core/block_validator.go b/core/block_validator.go index 028beadc491a..7b5f8f8ad518 100644 --- a/core/block_validator.go +++ b/core/block_validator.go @@ -17,6 +17,7 @@ package core import ( + "bytes" "fmt" "math/big" @@ -108,6 +109,15 @@ func (v *BlockValidator) ValidateState(block *types.Block, statedb *state.StateD return nil } +// ValidateReward validates the reward info of remote block with the reward processed locally +func (v *BlockValidator) ValidateReward(header *types.Header, localHeader *types.Header) error { + // verify rewards info + if !bytes.Equal(header.Rewards, localHeader.Rewards) { + return fmt.Errorf("invalid rewards (remote: %x local: %x)", header.Rewards, localHeader.Rewards) + } + return nil +} + // 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. diff --git a/core/blockchain.go b/core/blockchain.go index 41d4bbeb84e2..8a570e7fa758 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -43,7 +43,6 @@ import ( "github.com/ethereum/go-ethereum/metrics" "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/trie" - wemixminer "github.com/ethereum/go-ethereum/wemix/miner" lru "github.com/hashicorp/golang-lru" ) @@ -1608,11 +1607,6 @@ func (bc *BlockChain) insertChain(chain types.Blocks, verifySeals, setHead bool) parent = bc.GetHeader(block.ParentHash(), block.NumberU64()-1) } - // wemix: reward calculation uses governance contract, meaning - // the previous block is required. For fast sync, we need to wait for - // governance is initialized and try again. - retryCount := 2 - retry: statedb, err := state.New(parent.Root, bc.stateCache, bc.snaps) if err != nil { return it.index, err @@ -1642,13 +1636,12 @@ func (bc *BlockChain) insertChain(chain types.Blocks, verifySeals, setHead bool) // Process block using the parent state as reference point substart := time.Now() - receipts, logs, usedGas, fees, err := bc.processor.Process(block, statedb, bc.vmConfig) + appliedHeader, receipts, logs, usedGas, fees, err := bc.processor.Process(block, statedb, bc.vmConfig) if err != nil { bc.reportBlock(block, receipts, err) atomic.StoreUint32(&followupInterrupt, 1) return it.index, err } - // Update the metrics touched during block processing accountReadTimer.Update(statedb.AccountReads) // Account reads are complete, we can mark them storageReadTimer.Update(statedb.StorageReads) // Storage reads are complete, we can mark them @@ -1665,17 +1658,12 @@ func (bc *BlockChain) insertChain(chain types.Blocks, verifySeals, setHead bool) // Validate the state using the default validator substart = time.Now() if err := bc.validator.ValidateState(block, statedb, receipts, usedGas, fees); err != nil { - if retryCount--; !wemixminer.IsPoW() && retryCount > 0 { - // make sure the previous block exists in order to calculate rewards distribution - for try := 100; try > 0; try-- { - if _, _, err := wemixminer.CalculateRewards(block.Number(), big.NewInt(0), big.NewInt(100000000), nil); err == nil { - break - } - time.Sleep(100 * time.Millisecond) - } - goto retry - } + bc.reportBlock(block, receipts, err) + atomic.StoreUint32(&followupInterrupt, 1) + return it.index, err + } + if err := bc.validator.ValidateReward(block.Header(), appliedHeader); err != nil { bc.reportBlock(block, receipts, err) atomic.StoreUint32(&followupInterrupt, 1) return it.index, err diff --git a/core/blockchain_test.go b/core/blockchain_test.go index cda30f0f5860..568bc677ef29 100644 --- a/core/blockchain_test.go +++ b/core/blockchain_test.go @@ -159,17 +159,24 @@ func testBlockChainImport(chain types.Blocks, blockchain *BlockChain) error { if err != nil { return err } - receipts, _, usedGas, fees, err := blockchain.processor.Process(block, statedb, vm.Config{}) + appliedHeader, receipts, _, usedGas, fees, err := blockchain.processor.Process(block, statedb, vm.Config{}) if err != nil { blockchain.reportBlock(block, receipts, err) return err } + err = blockchain.validator.ValidateState(block, statedb, receipts, usedGas, fees) if err != nil { blockchain.reportBlock(block, receipts, err) return err } + err = blockchain.validator.ValidateReward(block.Header(), appliedHeader) + if err != nil { + blockchain.reportBlock(block, receipts, err) + return err + } + blockchain.chainmu.MustLock() rawdb.WriteTd(blockchain.db, block.Hash(), block.NumberU64(), new(big.Int).Add(block.Difficulty(), blockchain.GetTd(block.ParentHash(), block.NumberU64()-1))) rawdb.WriteBlock(blockchain.db, block) @@ -3770,3 +3777,28 @@ func TestSetCanonical(t *testing.T) { chain.SetCanonical(canon[DefaultCacheConfig.TriesInMemory-1]) verify(canon[DefaultCacheConfig.TriesInMemory-1]) } + +func TestRewardValidation(t *testing.T) { + // Configure and generate a sample block chain + var ( + db = rawdb.NewMemoryDatabase() + key, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") + address = crypto.PubkeyToAddress(key.PublicKey) + funds = big.NewInt(1000000000) + deleteAddr = common.Address{1} + gspec = &Genesis{ + Config: ¶ms.ChainConfig{ChainID: big.NewInt(1), EIP150Block: big.NewInt(0), EIP155Block: big.NewInt(2), HomesteadBlock: new(big.Int)}, + Alloc: GenesisAlloc{address: {Balance: funds}, deleteAddr: {Balance: new(big.Int)}}, + } + genesis = gspec.MustCommit(db) + ) + + blockchain, _ := NewBlockChain(db, nil, gspec.Config, ethash.NewFaker(), vm.Config{}, nil, nil) + defer blockchain.Stop() + + blocks, _ := GenerateChain(gspec.Config, genesis, ethash.NewFaker(), db, 1, nil) + + if _, err := blockchain.InsertChain(blocks); err != nil { + t.Fatal(err) + } +} diff --git a/core/state_processor.go b/core/state_processor.go index e2b2c1fc5efc..743d3d6f98ed 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -56,7 +56,7 @@ func NewStateProcessor(config *params.ChainConfig, bc *BlockChain, engine consen // Process returns the receipts and logs accumulated during the process and // returns the amount of gas that was used in the process. If any of the // transactions failed to execute due to insufficient gas it will return an error. -func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg vm.Config) (types.Receipts, []*types.Log, uint64, *big.Int, error) { +func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg vm.Config) (*types.Header, types.Receipts, []*types.Log, uint64, *big.Int, error) { var ( receipts types.Receipts usedGas = new(uint64) @@ -77,12 +77,12 @@ func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg for i, tx := range block.Transactions() { msg, err := tx.AsMessage(types.MakeSigner(p.config, header.Number), header.BaseFee) if err != nil { - return nil, nil, 0, big.NewInt(0), fmt.Errorf("could not apply tx %d [%v]: %w", i, tx.Hash().Hex(), err) + return nil, nil, nil, 0, big.NewInt(0), fmt.Errorf("could not apply tx %d [%v]: %w", i, tx.Hash().Hex(), err) } statedb.Prepare(tx.Hash(), i) receipt, err := applyTransaction(msg, p.config, p.bc, nil, gp, statedb, blockNumber, blockHash, tx, usedGas, fees, vmenv) if err != nil { - return nil, nil, 0, big.NewInt(0), fmt.Errorf("could not apply tx %d [%v]: %w", i, tx.Hash().Hex(), err) + return nil, nil, nil, 0, big.NewInt(0), fmt.Errorf("could not apply tx %d [%v]: %w", i, tx.Hash().Hex(), err) } receipts = append(receipts, receipt) allLogs = append(allLogs, receipt.Logs...) @@ -90,7 +90,7 @@ func (p *StateProcessor) Process(block *types.Block, statedb *state.StateDB, cfg // Finalize the block, applying any consensus engine specific extras (e.g. block rewards) p.engine.Finalize(p.bc, header, statedb, block.Transactions(), block.Uncles()) - return receipts, allLogs, *usedGas, fees, nil + return header, receipts, allLogs, *usedGas, fees, nil } func applyTransaction(msg types.Message, config *params.ChainConfig, bc ChainContext, author *common.Address, gp *GasPool, statedb *state.StateDB, blockNumber *big.Int, blockHash common.Hash, tx *types.Transaction, usedGas *uint64, fees *big.Int, evm *vm.EVM) (*types.Receipt, error) { diff --git a/core/types.go b/core/types.go index ae909a48fb1f..3c565d1118cf 100644 --- a/core/types.go +++ b/core/types.go @@ -34,6 +34,9 @@ type Validator interface { // ValidateState validates the given statedb and optionally the receipts and // gas used. ValidateState(block *types.Block, state *state.StateDB, receipts types.Receipts, usedGas uint64, fees *big.Int) error + + // ValidateReward validates the reward info of the block header with the reward processed in local + ValidateReward(header *types.Header, localHeader *types.Header) error } // Prefetcher is an interface for pre-caching transaction signatures and state. @@ -49,5 +52,5 @@ type Processor interface { // Process processes the state changes according to the Ethereum rules by running // the transaction messages using the statedb and applying any rewards to both // the processor (coinbase) and any included uncles. - Process(block *types.Block, statedb *state.StateDB, cfg vm.Config) (types.Receipts, []*types.Log, uint64, *big.Int, error) + Process(block *types.Block, statedb *state.StateDB, cfg vm.Config) (*types.Header, types.Receipts, []*types.Log, uint64, *big.Int, error) } diff --git a/eth/downloader/downloader.go b/eth/downloader/downloader.go index 9eef5dfd8d70..9537a5a3e7fa 100644 --- a/eth/downloader/downloader.go +++ b/eth/downloader/downloader.go @@ -384,6 +384,7 @@ func (d *Downloader) synchronise(id string, hash common.Hash, td, ttd *big.Int, // Post a user notification of the sync (only once per session) if atomic.CompareAndSwapInt32(&d.notified, 0, 1) { log.Info("Block synchronisation started") + defer log.Info("Block synchronisation finished") } if mode == SnapSync { // Snap sync uses the snapshot namespace to store potentially flakey data until diff --git a/eth/state_accessor.go b/eth/state_accessor.go index 98be012f3326..481909968fd0 100644 --- a/eth/state_accessor.go +++ b/eth/state_accessor.go @@ -131,7 +131,7 @@ func (eth *Ethereum) StateAtBlock(block *types.Block, reexec uint64, base *state if current = eth.blockchain.GetBlockByNumber(next); current == nil { return nil, fmt.Errorf("block #%d not found", next) } - _, _, _, _, err := eth.blockchain.Processor().Process(current, statedb, vm.Config{}) + _, _, _, _, _, err := eth.blockchain.Processor().Process(current, statedb, vm.Config{}) if err != nil { return nil, fmt.Errorf("processing block %d failed: %v", current.NumberU64(), err) } diff --git a/go.mod b/go.mod index 1956e64450e9..0e3cb402128d 100644 --- a/go.mod +++ b/go.mod @@ -60,6 +60,8 @@ require ( github.com/stretchr/testify v1.7.0 github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 github.com/tyler-smith/go-bip39 v1.0.1-0.20181017060643-dbb3b84ba2ef + github.com/yahoo/coname v0.0.0-20170609175141-84592ddf8673 + github.com/yoseplee/vrf v0.0.0-20210814110709-d1caf509310b go.etcd.io/etcd/api/v3 v3.5.2 go.etcd.io/etcd/client/v3 v3.5.2 go.etcd.io/etcd/server/v3 v3.5.2 @@ -72,8 +74,6 @@ require ( golang.org/x/tools v0.1.8-0.20211029000441-d6a9af8af023 gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce gopkg.in/urfave/cli.v1 v1.20.0 - github.com/yoseplee/vrf v0.0.0-20210814110709-d1caf509310b - github.com/yahoo/coname v0.0.0-20170609175141-84592ddf8673 ) require ( diff --git a/params/config.go b/params/config.go index 39d494868726..8847ddfdf93d 100644 --- a/params/config.go +++ b/params/config.go @@ -159,7 +159,13 @@ var ( LondonBlock: big.NewInt(0), PangyoBlock: big.NewInt(0), ApplepieBlock: big.NewInt(20_476_911), + BriocheBlock: big.NewInt(53_557_371), // 24-07-01 00:00:00 (UTC) expected Ethash: new(EthashConfig), + Brioche: &BriocheConfig{ + BlockReward: big.NewInt(1e18), + FirstHalving: big.NewInt(53_557_371), + HalvingPeriod: big.NewInt(63_115_200), + }, } // WemixTestnetChainConfig contains the chain parameters to run a node on the Wemix test network. @@ -180,7 +186,13 @@ var ( LondonBlock: big.NewInt(0), PangyoBlock: big.NewInt(10_000_000), ApplepieBlock: big.NewInt(26_240_268), + BriocheBlock: big.NewInt(60_537_845), // 24-06-17 02:00:00 (UTC) expected Ethash: new(EthashConfig), + Brioche: &BriocheConfig{ + BlockReward: big.NewInt(1e18), + FirstHalving: big.NewInt(60_537_845), + HalvingPeriod: big.NewInt(63_115_200), + }, } // SepoliaChainConfig contains the chain parameters to run a node on the Sepolia test network. @@ -303,16 +315,16 @@ var ( // // This configuration is intentionally not using keyed fields to force anyone // adding flags to the config to also have to set these fields. - AllEthashProtocolChanges = &ChainConfig{big.NewInt(1337), big.NewInt(0), nil, false, big.NewInt(0), common.Hash{}, big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), nil, nil, nil, nil, new(EthashConfig), nil} + AllEthashProtocolChanges = &ChainConfig{big.NewInt(1337), big.NewInt(0), nil, false, big.NewInt(0), common.Hash{}, big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), nil, nil, nil, nil, nil, new(EthashConfig), nil, nil} // AllCliqueProtocolChanges contains every protocol change (EIPs) introduced // and accepted by the Ethereum core developers into the Clique consensus. // // This configuration is intentionally not using keyed fields to force anyone // adding flags to the config to also have to set these fields. - AllCliqueProtocolChanges = &ChainConfig{big.NewInt(1337), big.NewInt(0), nil, false, big.NewInt(0), common.Hash{}, big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), nil, nil, nil, nil, nil, nil, &CliqueConfig{Period: 0, Epoch: 30000}} + AllCliqueProtocolChanges = &ChainConfig{big.NewInt(1337), big.NewInt(0), nil, false, big.NewInt(0), common.Hash{}, big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), nil, nil, nil, nil, nil, nil, nil, &CliqueConfig{Period: 0, Epoch: 30000}, nil} - TestChainConfig = &ChainConfig{big.NewInt(1), big.NewInt(0), nil, false, big.NewInt(0), common.Hash{}, big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), nil, nil, nil, nil, new(EthashConfig), nil} + TestChainConfig = &ChainConfig{big.NewInt(1), big.NewInt(0), nil, false, big.NewInt(0), common.Hash{}, big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), big.NewInt(0), nil, nil, nil, nil, nil, new(EthashConfig), nil, nil} TestRules = TestChainConfig.Rules(new(big.Int), false) ) @@ -395,19 +407,31 @@ type ChainConfig struct { MergeForkBlock *big.Int `json:"mergeForkBlock,omitempty"` // EIP-3675 (TheMerge) switch block (nil = no fork, 0 = already in merge proceedings) PangyoBlock *big.Int `json:"pangyoBlock,omitempty"` // Pangyo switch block (nil = no fork, 0 = already on pangyo) ApplepieBlock *big.Int `json:"applepieBlock,omitempty"` // Applepie switch block (nil = no fork, 0 = already on applepie) + BriocheBlock *big.Int `json:"briocheBlock,omitempty"` // Brioche switch block (nil = no fork, 0 = already on brioche) // TerminalTotalDifficulty is the amount of total difficulty reached by // the network that triggers the consensus upgrade. TerminalTotalDifficulty *big.Int `json:"terminalTotalDifficulty,omitempty"` // Various consensus engines - Ethash *EthashConfig `json:"ethash,omitempty"` - Clique *CliqueConfig `json:"clique,omitempty"` + Ethash *EthashConfig `json:"ethash,omitempty"` + Clique *CliqueConfig `json:"clique,omitempty"` + Brioche *BriocheConfig `json:"brioche,omitempty"` // if this config is nil, brioche halving is not applied } // EthashConfig is the consensus engine configs for proof-of-work based sealing. type EthashConfig struct{} +// Brioche halving configuration +type BriocheConfig struct { + // if the chain is on brioche hard fork, `RewardAmount` of gov contract is not used rather this `BlockReward` is used + BlockReward *big.Int `json:"blockReward,omitempty"` // if nil, then default block reward is 1e18 (=1 wemix) + FirstHalving *big.Int `json:"firstHalving,omitempty"` // if nil, then halving is not work + HalvingPeriod *big.Int `json:"halvingPeriod,omitempty"` // if nil, then halving is not work + LastHalving *big.Int `json:"lastHalving,omitempty"` // if nil, then halving goes on continuously + ZeroReward *big.Int `json:"zeroReward,omitempty"` // if nil, +} + // String implements the stringer interface, returning the consensus engine details. func (c *EthashConfig) String() string { return "ethash" @@ -435,7 +459,7 @@ func (c *ChainConfig) String() string { default: engine = "unknown" } - return fmt.Sprintf("{ChainID: %v Homestead: %v DAO: %v DAOSupport: %v EIP150: %v EIP155: %v EIP158: %v Byzantium: %v Constantinople: %v Petersburg: %v Istanbul: %v, Muir Glacier: %v, Berlin: %v, London: %v, Arrow Glacier: %v, MergeFork: %v, PangyoFork: %v, ApplepieFork: %v, Terminal TD: %v, Engine: %v}", + return fmt.Sprintf("{ChainID: %v Homestead: %v DAO: %v DAOSupport: %v EIP150: %v EIP155: %v EIP158: %v Byzantium: %v Constantinople: %v Petersburg: %v Istanbul: %v, Muir Glacier: %v, Berlin: %v, London: %v, Arrow Glacier: %v, MergeFork: %v, PangyoFork: %v, ApplepieFork: %v, BriocheFork: %v, Terminal TD: %v, Engine: %v}", c.ChainID, c.HomesteadBlock, c.DAOForkBlock, @@ -454,6 +478,7 @@ func (c *ChainConfig) String() string { c.MergeForkBlock, c.PangyoBlock, c.ApplepieBlock, + c.BriocheBlock, c.TerminalTotalDifficulty, engine, ) @@ -537,6 +562,10 @@ func (c *ChainConfig) IsArrowGlacier(num *big.Int) bool { return isForked(c.ArrowGlacierBlock, num) } +func (c *ChainConfig) IsBrioche(num *big.Int) bool { + return isForked(c.BriocheBlock, num) +} + // IsTerminalPoWBlock returns whether the given block is the last block of PoW stage. func (c *ChainConfig) IsTerminalPoWBlock(parentTotalDiff *big.Int, totalDiff *big.Int) bool { if c.TerminalTotalDifficulty == nil { diff --git a/wemix/admin.go b/wemix/admin.go index 5441dfa14f65..f0470b68c0c5 100644 --- a/wemix/admin.go +++ b/wemix/admin.go @@ -123,11 +123,12 @@ type rewardParameters struct { var ( // "Wemix Registry" - magic, _ = big.NewInt(0).SetString("0x57656d6978205265676973747279", 0) - etcdClusterName = "Wemix" - big0 = big.NewInt(0) - nilAddress = common.Address{} - admin *wemixAdmin + magic, _ = big.NewInt(0).SetString("0x57656d6978205265676973747279", 0) + etcdClusterName = "Wemix" + big0 = big.NewInt(0) + nilAddress = common.Address{} + defaultBriocheBlockReward = int64(1e18) + admin *wemixAdmin ErrAlreadyRunning = errors.New("already running") ErrExists = errors.New("already exists") @@ -1051,7 +1052,7 @@ func handleBlock94Rewards(height *big.Int, rp *rewardParameters, fees *big.Int) // distributeRewards divides the rewardAmount among members according to their // stakes, and allocates rewards to staker, ecoSystem, and maintenance accounts. -func distributeRewards(height *big.Int, rp *rewardParameters, fees *big.Int) ([]reward, error) { +func distributeRewards(height *big.Int, rp *rewardParameters, blockReward *big.Int, fees *big.Int) ([]reward, error) { dm := new(big.Int) for i := 0; i < len(rp.distributionMethod); i++ { dm.Add(dm, rp.distributionMethod[i]) @@ -1061,14 +1062,14 @@ func distributeRewards(height *big.Int, rp *rewardParameters, fees *big.Int) ([] } v10000 := big.NewInt(10000) - minerAmount := new(big.Int).Set(rp.rewardAmount) + minerAmount := new(big.Int).Set(blockReward) minerAmount.Div(minerAmount.Mul(minerAmount, rp.distributionMethod[0]), v10000) - stakerAmount := new(big.Int).Set(rp.rewardAmount) + stakerAmount := new(big.Int).Set(blockReward) stakerAmount.Div(stakerAmount.Mul(stakerAmount, rp.distributionMethod[1]), v10000) - ecoSystemAmount := new(big.Int).Set(rp.rewardAmount) + ecoSystemAmount := new(big.Int).Set(blockReward) ecoSystemAmount.Div(ecoSystemAmount.Mul(ecoSystemAmount, rp.distributionMethod[2]), v10000) // the rest goes to maintenance - maintenanceAmount := new(big.Int).Set(rp.rewardAmount) + maintenanceAmount := new(big.Int).Set(blockReward) maintenanceAmount.Sub(maintenanceAmount, minerAmount) maintenanceAmount.Sub(maintenanceAmount, stakerAmount) maintenanceAmount.Sub(maintenanceAmount, ecoSystemAmount) @@ -1102,7 +1103,7 @@ func distributeRewards(height *big.Int, rp *rewardParameters, fees *big.Int) ([] } d.Mul(d, vn) b.Sub(b, d) - for i, ix := 0, height.Int64()%int64(n); b.Cmp(v0) > 0; i, ix = i+1, (ix+1)%int64(n) { + for ix := height.Int64() % int64(n); b.Cmp(v0) > 0; ix = (ix + 1) % int64(n) { rewards[ix].Reward.Add(rewards[ix].Reward, v1) b.Sub(b, v1) } @@ -1146,7 +1147,7 @@ func distributeRewards(height *big.Int, rp *rewardParameters, fees *big.Int) ([] return rewards, nil } -func (ma *wemixAdmin) calculateRewards(num, blockReward, fees *big.Int, addBalance func(common.Address, *big.Int)) (coinbase *common.Address, rewards []byte, err error) { +func (ma *wemixAdmin) calculateRewards(config *params.ChainConfig, num, fees *big.Int, addBalance func(common.Address, *big.Int)) (coinbase *common.Address, rewards []byte, err error) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -1179,7 +1180,32 @@ func (ma *wemixAdmin) calculateRewards(num, blockReward, fees *big.Int, addBalan coinbase.SetBytes(rp.members[mix].Reward.Bytes()) } - rr, errr := distributeRewards(num, rp, fees) + // block reward + // - not brioche chain: use `EnvStorageImp.getBlockRewardAmount()` + // - brioche chain + // - config.Brioche.BlockReward != nil: config.Brioche.BlockReward + // - config.Brioche.BlockReward == nil: 1e18 + // - apply halving for BlockReward + var blockReward *big.Int + if config.IsBrioche(num) { + if config.Brioche != nil && config.Brioche.BlockReward != nil { + blockReward = big.NewInt(0).Set(config.Brioche.BlockReward) + } else { + blockReward = big.NewInt(defaultBriocheBlockReward) // default brioche block reward + } + if config.Brioche != nil && + config.Brioche.FirstHalving != nil && + config.Brioche.HalvingPeriod != nil && + num.Cmp(config.Brioche.FirstHalving) >= 0 { + past := big.NewInt(0).Set(num) + past.Sub(past, config.Brioche.FirstHalving) + blockReward = halveRewards(blockReward, config.Brioche.HalvingPeriod, past) + } + } else { + // if the wemix chain is not on brioche hard fork, use the `rewardAmount` from gov contract + blockReward = big.NewInt(0).Set(rp.rewardAmount) + } + rr, errr := distributeRewards(num, rp, blockReward, fees) if errr != nil { err = errr return @@ -1195,8 +1221,21 @@ func (ma *wemixAdmin) calculateRewards(num, blockReward, fees *big.Int, addBalan return } -func calculateRewards(num, blockReward, fees *big.Int, addBalance func(common.Address, *big.Int)) (*common.Address, []byte, error) { - return admin.calculateRewards(num, blockReward, fees, addBalance) +func calculateRewards(config *params.ChainConfig, num, fees *big.Int, addBalance func(common.Address, *big.Int)) (*common.Address, []byte, error) { + return admin.calculateRewards(config, num, fees, addBalance) +} + +func halveRewards(baseReward *big.Int, halvePeriod *big.Int, pastBlocks *big.Int) *big.Int { + result := big.NewInt(0).Set(baseReward) + past := big.NewInt(0).Set(pastBlocks) + for { + result = result.Div(result, big.NewInt(2)) + if past.Cmp(halvePeriod) < 0 { + break + } + past = past.Sub(past, halvePeriod) + } + return result } func verifyRewards(num *big.Int, rewards string) error { diff --git a/wemix/admin_test.go b/wemix/admin_test.go new file mode 100644 index 000000000000..d912dcf667d1 --- /dev/null +++ b/wemix/admin_test.go @@ -0,0 +1,48 @@ +package wemix + +import ( + "math/big" + "testing" +) + +func TestHalveRewards(t *testing.T) { + testcases := []struct { + reward *big.Int + period *big.Int + past *big.Int + expected *big.Int + }{ + // sample test + {big.NewInt(1e18), big.NewInt(100), big.NewInt(0), big.NewInt(5e17)}, + {big.NewInt(1e18), big.NewInt(100), big.NewInt(99), big.NewInt(5e17)}, + {big.NewInt(1e18), big.NewInt(100), big.NewInt(100), big.NewInt(25e16)}, + {big.NewInt(1e18), big.NewInt(100), big.NewInt(101), big.NewInt(25e16)}, + + // brioche halving test + {big.NewInt(1e18), big.NewInt(63115200), big.NewInt(0), big.NewInt(5e17)}, + {big.NewInt(1e18), big.NewInt(63115200), big.NewInt(63115200 - 1), big.NewInt(5e17)}, + {big.NewInt(1e18), big.NewInt(63115200), big.NewInt(63115200), big.NewInt(25e16)}, + {big.NewInt(1e18), big.NewInt(63115200), big.NewInt(63115200*2 - 1), big.NewInt(25e16)}, + {big.NewInt(1e18), big.NewInt(63115200), big.NewInt(63115200 * 2), big.NewInt(125e15)}, + {big.NewInt(1e18), big.NewInt(63115200), big.NewInt(63115200 * 3), big.NewInt(625e14)}, + {big.NewInt(1e18), big.NewInt(63115200), big.NewInt(63115200 * 4), big.NewInt(3125e13)}, + {big.NewInt(1e18), big.NewInt(63115200), big.NewInt(63115200 * 5), big.NewInt(15625e12)}, + {big.NewInt(1e18), big.NewInt(63115200), big.NewInt(63115200 * 6), big.NewInt(78125e11)}, + {big.NewInt(1e18), big.NewInt(63115200), big.NewInt(63115200 * 7), big.NewInt(390625e10)}, + {big.NewInt(1e18), big.NewInt(63115200), big.NewInt(63115200 * 8), big.NewInt(1953125e9)}, + {big.NewInt(1e18), big.NewInt(63115200), big.NewInt(63115200 * 9), big.NewInt(9765625e8)}, + {big.NewInt(1e18), big.NewInt(63115200), big.NewInt(63115200 * 10), big.NewInt(48828125e7)}, + {big.NewInt(1e18), big.NewInt(63115200), big.NewInt(63115200 * 11), big.NewInt(244140625e6)}, + {big.NewInt(1e18), big.NewInt(63115200), big.NewInt(63115200 * 12), big.NewInt(1220703125e5)}, + {big.NewInt(1e18), big.NewInt(63115200), big.NewInt(63115200 * 13), big.NewInt(6103515625e4)}, + {big.NewInt(1e18), big.NewInt(63115200), big.NewInt(63115200 * 14), big.NewInt(30517578125e3)}, + {big.NewInt(1e18), big.NewInt(63115200), big.NewInt(63115200 * 15), big.NewInt(152587890625e2)}, + } + + for _, testcase := range testcases { + halved := halveRewards(testcase.reward, testcase.period, testcase.past) + if testcase.expected.Cmp(halved) != 0 { + t.Errorf("halveRewards mismatched (expected=%v, actual=%v)", testcase.expected, halved) + } + } +} diff --git a/wemix/miner/miner.go b/wemix/miner/miner.go index 45abda5a5c59..0b369b3b063c 100644 --- a/wemix/miner/miner.go +++ b/wemix/miner/miner.go @@ -16,7 +16,7 @@ var ( AmPartnerFunc func() bool IsPartnerFunc func(string) bool AmHubFunc func(string) int - CalculateRewardsFunc func(*big.Int, *big.Int, *big.Int, func(common.Address, *big.Int)) (*common.Address, []byte, error) + CalculateRewardsFunc func(*params.ChainConfig, *big.Int, *big.Int, func(common.Address, *big.Int)) (*common.Address, []byte, error) VerifyRewardsFunc func(*big.Int, string) error GetCoinbaseFunc func(height *big.Int) (coinbase common.Address, err error) SignBlockFunc func(height *big.Int, hash common.Hash) (coinbase common.Address, sig []byte, err error) @@ -79,11 +79,11 @@ func IsPoW() bool { return params.ConsensusMethod == params.ConsensusPoW } -func CalculateRewards(num, blockReward, fees *big.Int, addBalance func(common.Address, *big.Int)) (*common.Address, []byte, error) { +func CalculateRewards(config *params.ChainConfig, num, fees *big.Int, addBalance func(common.Address, *big.Int)) (*common.Address, []byte, error) { if CalculateRewardsFunc == nil { return nil, nil, ErrNotInitialized } else { - return CalculateRewardsFunc(num, blockReward, fees, addBalance) + return CalculateRewardsFunc(config, num, fees, addBalance) } } diff --git a/wemix/rewards_test.go b/wemix/rewards_test.go index 0737abd492c1..ea90b90f42a0 100644 --- a/wemix/rewards_test.go +++ b/wemix/rewards_test.go @@ -4,12 +4,41 @@ package wemix import ( "encoding/json" + "github.com/ethereum/go-ethereum/common" "math/big" + "strconv" "testing" - - "github.com/ethereum/go-ethereum/common" + "time" ) +func TestBlockMintingQuantity(t *testing.T) { + minted, _ := big.NewInt(0).SetString("379886454000000000000000000", 0) // 379,886,454 wemix + annualReward, _ := big.NewInt(0).SetString("500000000000000000", 0) // 0.5 wemix + t.Logf("initial minted = %d", minted) + t.Logf("reward = %d", annualReward) + for i := 2024; i < 2054; i += 2 { + minted = new(big.Int).Add(minted, new(big.Int).Mul(annualReward, big.NewInt(63115200))) + annualReward = new(big.Int).Div(annualReward, big.NewInt(2)) + t.Logf("minted = %d", minted) + t.Logf("reward = %d", annualReward) + } + for i := 2054; i < 2100; i += 2 { + minted = new(big.Int).Add(minted, new(big.Int).Mul(annualReward, big.NewInt(63115200))) + } + t.Logf("total minted = %d", minted) +} + +func TestHalvesPeriod(t *testing.T) { + halvesTime := big.NewInt(1719792000) // 2024-07-01 00:00:00 (UTC) + blockPeriod := big.NewInt(63_115_200) // 86400 * 730.5 + loc, _ := time.LoadLocation("UTC") + for i := 0; i < 17; i++ { + tm, _ := strconv.ParseInt(halvesTime.String(), 10, 64) + t.Logf("~ %v(%d)", time.Unix(tm, 0).In(loc), halvesTime) + halvesTime.Add(halvesTime, blockPeriod) + } +} + // TestDistributeRewards tests the DistributeRewards function func TestDistributeRewards(t *testing.T) { @@ -135,11 +164,35 @@ func TestDistributeRewards(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Call the distributeRewards function - rewards, err := distributeRewards(tt.height, tt.rp, tt.fees) + rewards, err := distributeRewards(tt.height, tt.rp, big.NewInt(defaultBriocheBlockReward), tt.fees) rewardsString, _ := json.Marshal(rewards) if string(rewardsString) != tt.want { t.Errorf("distributeRewards() failed: %v, %v <-> %v", err, tt.want, string(rewardsString)) } + + distTotal := big.NewInt(0) + for _, dist := range tt.rp.distributionMethod { + distTotal.Add(distTotal, dist) + } + totalRewards := big.NewInt(0) + memberRewards := big.NewInt(0) + for i, reward := range rewards { + totalRewards.Add(totalRewards, reward.Reward) + if i < len(tt.rp.members) { + memberRewards.Add(memberRewards, reward.Reward) + } + } + totalAmount := big.NewInt(0).Set(tt.rp.rewardAmount) + totalAmount.Add(totalAmount, tt.fees) + memberAmount := big.NewInt(0).Set(tt.rp.rewardAmount) + memberAmount = memberAmount.Mul(memberAmount, tt.rp.distributionMethod[0]) + memberAmount = memberAmount.Div(memberAmount, distTotal) + if memberRewards.Cmp(memberAmount) != 0 { + t.Errorf("members reward amount mismatched! sum=%d, rewards=%d", memberRewards, memberAmount) + } + if totalRewards.Cmp(totalAmount) != 0 { + t.Errorf("total reward amount mismatched! sum=%d, rewards=%d", totalRewards, totalAmount) + } }) } }