mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-07-10 15:04:27 +00:00
check yourturn again during prepare (#72)
This commit is contained in:
parent
d55229677d
commit
ee025383c1
5 changed files with 152 additions and 82 deletions
|
|
@ -2,7 +2,6 @@ package engine_v2
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"math/big"
|
"math/big"
|
||||||
|
|
@ -200,6 +199,35 @@ func (x *XDPoS_v2) Initial(chain consensus.ChainReader, header *types.Header) er
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if it's my turm to mine a block. Note: The second return value `preIndex` is useless in V2 engine
|
||||||
|
func (x *XDPoS_v2) YourTurn(chain consensus.ChainReader, parent *types.Header, signer common.Address) (bool, error) {
|
||||||
|
x.lock.RLock()
|
||||||
|
defer x.lock.RUnlock()
|
||||||
|
|
||||||
|
if !x.isInitilised {
|
||||||
|
err := x.Initial(chain, parent)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("[YourTurn] Error while initialising last v2 variables", "ParentBlockHash", parent.Hash(), "Error", err)
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
x.isInitilised = true
|
||||||
|
}
|
||||||
|
|
||||||
|
waitedTime := time.Now().Unix() - parent.Time.Int64()
|
||||||
|
if waitedTime < int64(x.config.V2.MinePeriod) {
|
||||||
|
log.Trace("[YourTurn] wait after mine period", "minePeriod", x.config.V2.MinePeriod, "waitedTime", waitedTime)
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
round := x.currentRound
|
||||||
|
isMyTurn, err := x.checkYourturnWithinFinalisedMasternodes(chain, round, parent, signer)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("[Yourturn] Error while checking if i am qualified to mine", "round", round, "error", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return isMyTurn, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Prepare implements consensus.Engine, preparing all the consensus fields of the
|
// Prepare implements consensus.Engine, preparing all the consensus fields of the
|
||||||
// header for running the transactions on top.
|
// header for running the transactions on top.
|
||||||
func (x *XDPoS_v2) Prepare(chain consensus.ChainReader, header *types.Header) error {
|
func (x *XDPoS_v2) Prepare(chain consensus.ChainReader, header *types.Header) error {
|
||||||
|
|
@ -210,7 +238,6 @@ func (x *XDPoS_v2) Prepare(chain consensus.ChainReader, header *types.Header) er
|
||||||
x.lock.RUnlock()
|
x.lock.RUnlock()
|
||||||
|
|
||||||
if header.ParentHash != highestQC.ProposedBlockInfo.Hash {
|
if header.ParentHash != highestQC.ProposedBlockInfo.Hash {
|
||||||
fmt.Println("[Prepare] parent hash and QC hash does not match", "blockNum", header.Number, "parentHash", header.ParentHash, "QCHash", highestQC.ProposedBlockInfo.Hash, "QCNumber", highestQC.ProposedBlockInfo.Number)
|
|
||||||
log.Warn("[Prepare] parent hash and QC hash does not match", "blockNum", header.Number, "parentHash", header.ParentHash, "QCHash", highestQC.ProposedBlockInfo.Hash, "QCNumber", highestQC.ProposedBlockInfo.Number)
|
log.Warn("[Prepare] parent hash and QC hash does not match", "blockNum", header.Number, "parentHash", header.ParentHash, "QCHash", highestQC.ProposedBlockInfo.Hash, "QCNumber", highestQC.ProposedBlockInfo.Number)
|
||||||
return consensus.ErrNotReadyToPropose
|
return consensus.ErrNotReadyToPropose
|
||||||
}
|
}
|
||||||
|
|
@ -230,13 +257,26 @@ func (x *XDPoS_v2) Prepare(chain consensus.ChainReader, header *types.Header) er
|
||||||
|
|
||||||
number := header.Number.Uint64()
|
number := header.Number.Uint64()
|
||||||
parent := chain.GetHeader(header.ParentHash, number-1)
|
parent := chain.GetHeader(header.ParentHash, number-1)
|
||||||
|
|
||||||
log.Info("Preparing new block!", "Number", number, "Parent Hash", parent.Hash())
|
log.Info("Preparing new block!", "Number", number, "Parent Hash", parent.Hash())
|
||||||
if parent == nil {
|
if parent == nil {
|
||||||
return consensus.ErrUnknownAncestor
|
return consensus.ErrUnknownAncestor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
x.signLock.RLock()
|
||||||
|
signer := x.signer
|
||||||
|
x.signLock.RUnlock()
|
||||||
|
|
||||||
|
isMyTurn, err := x.checkYourturnWithinFinalisedMasternodes(chain, currentRound, parent, signer)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("[Prepare] Error while checking if it's still my turn to mine", "round", currentRound, "ParentHash", parent.Hash().Hex(), "ParentNumber", parent.Number.Uint64(), "error", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !isMyTurn {
|
||||||
|
return consensus.ErrNotReadyToMine
|
||||||
|
}
|
||||||
// Set the correct difficulty
|
// Set the correct difficulty
|
||||||
header.Difficulty = x.calcDifficulty(chain, parent, x.signer)
|
header.Difficulty = x.calcDifficulty(chain, parent, signer)
|
||||||
log.Debug("CalcDifficulty ", "number", header.Number, "difficulty", header.Difficulty)
|
log.Debug("CalcDifficulty ", "number", header.Number, "difficulty", header.Difficulty)
|
||||||
|
|
||||||
isEpochSwitchBlock, _, err := x.IsEpochSwitch(header)
|
isEpochSwitchBlock, _, err := x.IsEpochSwitch(header)
|
||||||
|
|
@ -268,10 +308,6 @@ func (x *XDPoS_v2) Prepare(chain consensus.ChainReader, header *types.Header) er
|
||||||
header.Time = big.NewInt(time.Now().Unix())
|
header.Time = big.NewInt(time.Now().Unix())
|
||||||
}
|
}
|
||||||
|
|
||||||
x.signLock.RLock()
|
|
||||||
signer := x.signer
|
|
||||||
x.signLock.RUnlock()
|
|
||||||
|
|
||||||
if header.Coinbase != signer {
|
if header.Coinbase != signer {
|
||||||
log.Error("[Prepare] The mined blocker header coinbase address mismatch with waller address", "headerCoinbase", header.Coinbase.Hex(), "WalletAddress", signer.Hex())
|
log.Error("[Prepare] The mined blocker header coinbase address mismatch with waller address", "headerCoinbase", header.Coinbase.Hex(), "WalletAddress", signer.Hex())
|
||||||
return consensus.ErrCoinbaseMismatch
|
return consensus.ErrCoinbaseMismatch
|
||||||
|
|
@ -372,77 +408,6 @@ func (x *XDPoS_v2) calcDifficulty(chain consensus.ChainReader, parent *types.Hea
|
||||||
return big.NewInt(1)
|
return big.NewInt(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if it's my turm to mine a block. Note: The second return value `preIndex` is useless in V2 engine
|
|
||||||
func (x *XDPoS_v2) YourTurn(chain consensus.ChainReader, parent *types.Header, signer common.Address) (bool, error) {
|
|
||||||
x.lock.RLock()
|
|
||||||
defer x.lock.RUnlock()
|
|
||||||
|
|
||||||
if !x.isInitilised {
|
|
||||||
err := x.Initial(chain, parent)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("[YourTurn] Error while initialising last v2 variables", "ParentBlockHash", parent.Hash(), "Error", err)
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
x.isInitilised = true
|
|
||||||
}
|
|
||||||
|
|
||||||
waitedTime := time.Now().Unix() - parent.Time.Int64()
|
|
||||||
if waitedTime < int64(x.config.V2.MinePeriod) {
|
|
||||||
log.Trace("[YourTurn] wait after mine period", "minePeriod", x.config.V2.MinePeriod, "waitedTime", waitedTime)
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
round := x.currentRound
|
|
||||||
isEpochSwitch, _, err := x.isEpochSwitchAtRound(round, parent)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("[YourTurn] check epoch switch at round failed", "Error", err)
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
var masterNodes []common.Address
|
|
||||||
if isEpochSwitch {
|
|
||||||
if x.config.V2.SwitchBlock.Cmp(parent.Number) == 0 {
|
|
||||||
// the initial master nodes of v1->v2 switch contains penalties node
|
|
||||||
_, _, masterNodes, err = x.getExtraFields(parent)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("[YourTurn] Cannot find snapshot at gap num of last V1", "err", err, "number", x.config.V2.SwitchBlock.Uint64())
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
masterNodes, _, err = x.calcMasternodes(chain, big.NewInt(0).Add(parent.Number, big.NewInt(1)), parent.Hash())
|
|
||||||
if err != nil {
|
|
||||||
log.Error("[YourTurn] Cannot calcMasternodes at gap num ", "err", err, "parent number", parent.Number)
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// this block and parent belong to the same epoch
|
|
||||||
masterNodes = x.GetMasternodes(chain, parent)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(masterNodes) == 0 {
|
|
||||||
log.Error("[YourTurn] Fail to find any master nodes from current block round epoch", "Hash", parent.Hash(), "CurrentRound", round, "Number", parent.Number)
|
|
||||||
return false, errors.New("masternodes not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
curIndex := utils.Position(masterNodes, signer)
|
|
||||||
if curIndex == -1 {
|
|
||||||
log.Debug("[YourTurn] Not authorised signer", "MN", masterNodes, "Hash", parent.Hash(), "signer", signer)
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, s := range masterNodes {
|
|
||||||
log.Debug("[YourTurn] Masternode:", "index", i, "address", s.String(), "parentBlockNum", parent.Number)
|
|
||||||
}
|
|
||||||
|
|
||||||
leaderIndex := uint64(round) % x.config.Epoch % uint64(len(masterNodes))
|
|
||||||
if masterNodes[leaderIndex] != signer {
|
|
||||||
log.Debug("[YourTurn] Not my turn", "curIndex", curIndex, "leaderIndex", leaderIndex, "Hash", parent.Hash(), "masterNodes[leaderIndex]", masterNodes[leaderIndex], "signer", signer)
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (x *XDPoS_v2) IsAuthorisedAddress(chain consensus.ChainReader, header *types.Header, address common.Address) bool {
|
func (x *XDPoS_v2) IsAuthorisedAddress(chain consensus.ChainReader, header *types.Header, address common.Address) bool {
|
||||||
x.lock.RLock()
|
x.lock.RLock()
|
||||||
defer x.lock.RUnlock()
|
defer x.lock.RUnlock()
|
||||||
|
|
|
||||||
65
consensus/XDPoS/engines/engine_v2/mining.go
Normal file
65
consensus/XDPoS/engines/engine_v2/mining.go
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
package engine_v2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"math/big"
|
||||||
|
|
||||||
|
"github.com/XinFinOrg/XDPoSChain/common"
|
||||||
|
"github.com/XinFinOrg/XDPoSChain/consensus"
|
||||||
|
"github.com/XinFinOrg/XDPoSChain/consensus/XDPoS/utils"
|
||||||
|
"github.com/XinFinOrg/XDPoSChain/core/types"
|
||||||
|
"github.com/XinFinOrg/XDPoSChain/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Using parent and current round to find the finalised master node list(with penalties applied from last epoch)
|
||||||
|
func (x *XDPoS_v2) checkYourturnWithinFinalisedMasternodes(chain consensus.ChainReader, round utils.Round, parent *types.Header, signer common.Address) (bool, error) {
|
||||||
|
isEpochSwitch, _, err := x.isEpochSwitchAtRound(round, parent)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("[checkYourturnWithinFinalisedMasternodes] check epoch switch at round failed", "Error", err)
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
var masterNodes []common.Address
|
||||||
|
if isEpochSwitch {
|
||||||
|
if x.config.V2.SwitchBlock.Cmp(parent.Number) == 0 {
|
||||||
|
// the initial master nodes of v1->v2 switch contains penalties node
|
||||||
|
_, _, masterNodes, err = x.getExtraFields(parent)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("[checkYourturnWithinFinalisedMasternodes] Cannot find snapshot at gap num of last V1", "err", err, "number", x.config.V2.SwitchBlock.Uint64())
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
masterNodes, _, err = x.calcMasternodes(chain, big.NewInt(0).Add(parent.Number, big.NewInt(1)), parent.Hash())
|
||||||
|
if err != nil {
|
||||||
|
log.Error("[checkYourturnWithinFinalisedMasternodes] Cannot calcMasternodes at gap num ", "err", err, "parent number", parent.Number)
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// this block and parent belong to the same epoch
|
||||||
|
masterNodes = x.GetMasternodes(chain, parent)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(masterNodes) == 0 {
|
||||||
|
log.Error("[checkYourturnWithinFinalisedMasternodes] Fail to find any master nodes from current block round epoch", "Hash", parent.Hash(), "CurrentRound", round, "Number", parent.Number)
|
||||||
|
return false, errors.New("masternodes not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
curIndex := utils.Position(masterNodes, signer)
|
||||||
|
if curIndex == -1 {
|
||||||
|
log.Debug("[checkYourturnWithinFinalisedMasternodes] Not authorised signer", "MN", masterNodes, "Hash", parent.Hash(), "signer", signer)
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, s := range masterNodes {
|
||||||
|
log.Debug("[checkYourturnWithinFinalisedMasternodes] Masternode:", "index", i, "address", s.String(), "parentBlockNum", parent.Number)
|
||||||
|
}
|
||||||
|
|
||||||
|
leaderIndex := uint64(round) % x.config.Epoch % uint64(len(masterNodes))
|
||||||
|
if masterNodes[leaderIndex] != signer {
|
||||||
|
log.Debug("[checkYourturnWithinFinalisedMasternodes] Not my turn", "curIndex", curIndex, "leaderIndex", leaderIndex, "Hash", parent.Hash().Hex(), "masterNodes[leaderIndex]", masterNodes[leaderIndex], "signer", signer)
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug("[checkYourturnWithinFinalisedMasternodes] Yes, it's my turn based on parent block", "ParentHash", parent.Hash().Hex(), "ParentBlockNumber", parent.Number.Uint64())
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
@ -41,5 +41,7 @@ var (
|
||||||
|
|
||||||
ErrNotReadyToPropose = errors.New("not ready to propose, QC is not ready")
|
ErrNotReadyToPropose = errors.New("not ready to propose, QC is not ready")
|
||||||
|
|
||||||
|
ErrNotReadyToMine = errors.New("Not ready to mine, it's not your turn")
|
||||||
|
|
||||||
ErrCoinbaseMismatch = errors.New("Block Coinbase address does not match its wallte address")
|
ErrCoinbaseMismatch = errors.New("Block Coinbase address does not match its wallte address")
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -125,15 +125,38 @@ func TestUpdateMasterNodes(t *testing.T) {
|
||||||
assert.Equal(t, int(snap.Number), 1350)
|
assert.Equal(t, int(snap.Number), 1350)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPrepare(t *testing.T) {
|
func TestPrepareFail(t *testing.T) {
|
||||||
config := params.TestXDPoSMockChainConfig
|
config := params.TestXDPoSMockChainConfig
|
||||||
blockchain, _, currentBlock, signer, _, _ := PrepareXDCTestBlockChainForV2Engine(t, int(config.XDPoS.Epoch), config, 0)
|
blockchain, _, currentBlock, signer, _, _ := PrepareXDCTestBlockChainForV2Engine(t, int(config.XDPoS.Epoch), config, 0)
|
||||||
adaptor := blockchain.Engine().(*XDPoS.XDPoS)
|
adaptor := blockchain.Engine().(*XDPoS.XDPoS)
|
||||||
|
|
||||||
_, err := adaptor.YourTurn(blockchain, currentBlock.Header(), common.HexToAddress("xdc0278C350152e15fa6FFC712a5A73D704Ce73E2E1"))
|
|
||||||
assert.Nil(t, err)
|
|
||||||
tstamp := time.Now().Unix()
|
tstamp := time.Now().Unix()
|
||||||
|
|
||||||
|
notReadyToProposeHeader := &types.Header{
|
||||||
|
ParentHash: currentBlock.Hash(),
|
||||||
|
Number: big.NewInt(int64(901)),
|
||||||
|
GasLimit: params.TargetGasLimit,
|
||||||
|
Time: big.NewInt(tstamp),
|
||||||
|
Coinbase: signer,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := adaptor.Prepare(blockchain, notReadyToProposeHeader)
|
||||||
|
assert.Equal(t, consensus.ErrNotReadyToPropose, err)
|
||||||
|
|
||||||
|
notReadyToMine := &types.Header{
|
||||||
|
ParentHash: currentBlock.Hash(),
|
||||||
|
Number: big.NewInt(int64(901)),
|
||||||
|
GasLimit: params.TargetGasLimit,
|
||||||
|
Time: big.NewInt(tstamp),
|
||||||
|
Coinbase: signer,
|
||||||
|
}
|
||||||
|
// trigger initial which will set the highestQC
|
||||||
|
_, err = adaptor.YourTurn(blockchain, currentBlock.Header(), signer)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
err = adaptor.Prepare(blockchain, notReadyToMine)
|
||||||
|
assert.Equal(t, consensus.ErrNotReadyToMine, err)
|
||||||
|
|
||||||
|
adaptor.EngineV2.SetNewRoundFaker(blockchain, utils.Round(4), false)
|
||||||
header901WithoutCoinbase := &types.Header{
|
header901WithoutCoinbase := &types.Header{
|
||||||
ParentHash: currentBlock.Hash(),
|
ParentHash: currentBlock.Hash(),
|
||||||
Number: big.NewInt(int64(901)),
|
Number: big.NewInt(int64(901)),
|
||||||
|
|
@ -143,6 +166,17 @@ func TestPrepare(t *testing.T) {
|
||||||
|
|
||||||
err = adaptor.Prepare(blockchain, header901WithoutCoinbase)
|
err = adaptor.Prepare(blockchain, header901WithoutCoinbase)
|
||||||
assert.Equal(t, consensus.ErrCoinbaseMismatch, err)
|
assert.Equal(t, consensus.ErrCoinbaseMismatch, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPrepareHappyPath(t *testing.T) {
|
||||||
|
config := params.TestXDPoSMockChainConfig
|
||||||
|
blockchain, _, currentBlock, signer, _, _ := PrepareXDCTestBlockChainForV2Engine(t, int(config.XDPoS.Epoch), config, 0)
|
||||||
|
adaptor := blockchain.Engine().(*XDPoS.XDPoS)
|
||||||
|
// trigger initial
|
||||||
|
_, err := adaptor.YourTurn(blockchain, currentBlock.Header(), signer)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
tstamp := time.Now().Unix()
|
||||||
|
|
||||||
header901 := &types.Header{
|
header901 := &types.Header{
|
||||||
ParentHash: currentBlock.Hash(),
|
ParentHash: currentBlock.Hash(),
|
||||||
|
|
@ -152,6 +186,7 @@ func TestPrepare(t *testing.T) {
|
||||||
Coinbase: signer,
|
Coinbase: signer,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
adaptor.EngineV2.SetNewRoundFaker(blockchain, utils.Round(4), false)
|
||||||
err = adaptor.Prepare(blockchain, header901)
|
err = adaptor.Prepare(blockchain, header901)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
|
@ -169,6 +204,6 @@ func TestPrepare(t *testing.T) {
|
||||||
var decodedExtraField utils.ExtraFields_v2
|
var decodedExtraField utils.ExtraFields_v2
|
||||||
err = utils.DecodeBytesExtraFields(header901.Extra, &decodedExtraField)
|
err = utils.DecodeBytesExtraFields(header901.Extra, &decodedExtraField)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
assert.Equal(t, utils.Round(1), decodedExtraField.Round)
|
assert.Equal(t, utils.Round(4), decodedExtraField.Round)
|
||||||
assert.Equal(t, utils.Round(0), decodedExtraField.QuorumCert.ProposedBlockInfo.Round)
|
assert.Equal(t, utils.Round(0), decodedExtraField.QuorumCert.ProposedBlockInfo.Round)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,9 @@ func TestHookPenaltyV2Mining(t *testing.T) {
|
||||||
Time: header6300.Time,
|
Time: header6300.Time,
|
||||||
Coinbase: signer,
|
Coinbase: signer,
|
||||||
}
|
}
|
||||||
|
// Force to make the node to be at its round to mine, otherwise won't pass the yourturn masternodes check
|
||||||
|
// We have 5 nodes in total and the node signer is always at the 4th(last) in the list. Hence int(config.XDPoS.Epoch)*7+4-900, the +4 means is to force to next 4 round and -900 is the relative round number to block number int(config.XDPoS.Epoch)*7
|
||||||
|
adaptor.EngineV2.SetNewRoundFaker(blockchain, utils.Round(int(config.XDPoS.Epoch)*7+4-900), false)
|
||||||
err = adaptor.Prepare(blockchain, headerMining)
|
err = adaptor.Prepare(blockchain, headerMining)
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
assert.Equal(t, 1, len(headerMining.Penalties)/common.AddressLength)
|
assert.Equal(t, 1, len(headerMining.Penalties)/common.AddressLength)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue