mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-07-16 18:00:46 +00:00
Xin 127 initial v2 function and snapshot (#42)
* initial v2 and v2 snapshot * update based on pr feedback * add test for initial v2 parameters * remove comment
This commit is contained in:
parent
e72d4866df
commit
646042a651
7 changed files with 241 additions and 285 deletions
|
|
@ -278,7 +278,10 @@ func (x *XDPoS) GetMasternodesByNumber(chain consensus.ChainReader, blockNumber
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *XDPoS) YourTurn(chain consensus.ChainReader, parent *types.Header, signer common.Address) (bool, error) {
|
func (x *XDPoS) YourTurn(chain consensus.ChainReader, parent *types.Header, signer common.Address) (bool, error) {
|
||||||
switch x.config.BlockConsensusVersion(parent.Number) {
|
if parent.Number.Cmp(x.config.XDPoSV2Block) == 0 {
|
||||||
|
x.initialV2(chain, parent)
|
||||||
|
}
|
||||||
|
switch x.config.BlockConsensusVersion(big.NewInt(parent.Number.Int64() + 1)) {
|
||||||
case params.ConsensusEngineVersion2:
|
case params.ConsensusEngineVersion2:
|
||||||
return x.EngineV2.YourTurn(chain, parent, signer)
|
return x.EngineV2.YourTurn(chain, parent, signer)
|
||||||
default: // Default "v1"
|
default: // Default "v1"
|
||||||
|
|
@ -296,7 +299,7 @@ func (x *XDPoS) GetValidator(creator common.Address, chain consensus.ChainReader
|
||||||
func (x *XDPoS) UpdateMasternodes(chain consensus.ChainReader, header *types.Header, ms []utils.Masternode) error {
|
func (x *XDPoS) UpdateMasternodes(chain consensus.ChainReader, header *types.Header, ms []utils.Masternode) error {
|
||||||
switch x.config.BlockConsensusVersion(header.Number) {
|
switch x.config.BlockConsensusVersion(header.Number) {
|
||||||
case params.ConsensusEngineVersion2:
|
case params.ConsensusEngineVersion2:
|
||||||
return nil
|
return x.EngineV2.UpdateMasternodes(chain, header, ms)
|
||||||
default: // Default "v1"
|
default: // Default "v1"
|
||||||
return x.EngineV1.UpdateMasternodes(chain, header, ms)
|
return x.EngineV1.UpdateMasternodes(chain, header, ms)
|
||||||
}
|
}
|
||||||
|
|
@ -361,7 +364,7 @@ func (x *XDPoS) GetSnapshot(chain consensus.ChainReader, header *types.Header) (
|
||||||
return &utils.PublicApiSnapshot{
|
return &utils.PublicApiSnapshot{
|
||||||
Number: sp.Number,
|
Number: sp.Number,
|
||||||
Hash: sp.Hash,
|
Hash: sp.Hash,
|
||||||
Signers: sp.MasterNodes,
|
Signers: sp.GetMappedMasterNodes(),
|
||||||
}, err
|
}, err
|
||||||
default: // Default "v1"
|
default: // Default "v1"
|
||||||
sp, err := x.EngineV1.GetSnapshot(chain, header)
|
sp, err := x.EngineV1.GetSnapshot(chain, header)
|
||||||
|
|
@ -439,6 +442,14 @@ func (x *XDPoS) GetCachedSigningTxs(hash common.Hash) (interface{}, bool) {
|
||||||
}
|
}
|
||||||
|
|
||||||
//V2
|
//V2
|
||||||
|
func (x *XDPoS) initialV2(chain consensus.ChainReader, header *types.Header) error {
|
||||||
|
checkpointBlockNumber := header.Number.Uint64() - header.Number.Uint64()%x.config.Epoch
|
||||||
|
checkpointHeader := chain.GetHeaderByNumber(checkpointBlockNumber)
|
||||||
|
masternodes := x.EngineV1.GetMasternodesFromCheckpointHeader(checkpointHeader)
|
||||||
|
x.EngineV2.Initial(chain, header, masternodes)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (x *XDPoS) VerifyVote(*utils.Vote) error {
|
func (x *XDPoS) VerifyVote(*utils.Vote) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ type XDPoS_v2 struct {
|
||||||
config *params.XDPoSConfig // Consensus engine configuration parameters
|
config *params.XDPoSConfig // Consensus engine configuration parameters
|
||||||
db ethdb.Database // Database to store and retrieve snapshot checkpoints
|
db ethdb.Database // Database to store and retrieve snapshot checkpoints
|
||||||
|
|
||||||
recents *lru.ARCCache // Snapshots for recent block to speed up reorgs
|
snapshots *lru.ARCCache // Snapshots for gap block
|
||||||
signatures *lru.ARCCache // Signatures of recent blocks to speed up mining
|
signatures *lru.ARCCache // Signatures of recent blocks to speed up mining
|
||||||
epochSwitches *lru.ARCCache // infos of epoch: master nodes, epoch switch block info, parent of that info
|
epochSwitches *lru.ARCCache // infos of epoch: master nodes, epoch switch block info, parent of that info
|
||||||
|
|
||||||
|
|
@ -60,7 +60,7 @@ func New(config *params.XDPoSConfig, db ethdb.Database) *XDPoS_v2 {
|
||||||
timer := countdown.NewCountDown(duration)
|
timer := countdown.NewCountDown(duration)
|
||||||
timeoutPool := utils.NewPool(config.V2.CertThreshold)
|
timeoutPool := utils.NewPool(config.V2.CertThreshold)
|
||||||
|
|
||||||
recents, _ := lru.NewARC(utils.InmemorySnapshots)
|
snapshots, _ := lru.NewARC(utils.InmemorySnapshots)
|
||||||
signatures, _ := lru.NewARC(utils.InmemorySnapshots)
|
signatures, _ := lru.NewARC(utils.InmemorySnapshots)
|
||||||
epochSwitches, _ := lru.NewARC(int(utils.InmemoryEpochs))
|
epochSwitches, _ := lru.NewARC(int(utils.InmemoryEpochs))
|
||||||
|
|
||||||
|
|
@ -70,7 +70,7 @@ func New(config *params.XDPoSConfig, db ethdb.Database) *XDPoS_v2 {
|
||||||
db: db,
|
db: db,
|
||||||
signatures: signatures,
|
signatures: signatures,
|
||||||
|
|
||||||
recents: recents,
|
snapshots: snapshots,
|
||||||
epochSwitches: epochSwitches,
|
epochSwitches: epochSwitches,
|
||||||
timeoutWorker: timer,
|
timeoutWorker: timer,
|
||||||
BroadcastCh: make(chan interface{}),
|
BroadcastCh: make(chan interface{}),
|
||||||
|
|
@ -106,40 +106,55 @@ type SignerFn func(accounts.Account, []byte) ([]byte, error)
|
||||||
sigHash returns the hash which is used as input for the delegated-proof-of-stake
|
sigHash returns the hash which is used as input for the delegated-proof-of-stake
|
||||||
signing. It is the hash of the entire header apart from the 65 byte signature
|
signing. It is the hash of the entire header apart from the 65 byte signature
|
||||||
contained at the end of the extra data.
|
contained at the end of the extra data.
|
||||||
|
|
||||||
Note, the method requires the extra data to be at least 65 bytes, otherwise it
|
|
||||||
panics. This is done to avoid accidentally using both forms (signature present
|
|
||||||
or not), which could be abused to produce different hashes for the same header.
|
|
||||||
*/
|
*/
|
||||||
func (x *XDPoS_v2) SignHash(header *types.Header) (hash common.Hash) {
|
func (x *XDPoS_v2) SignHash(header *types.Header) (hash common.Hash) {
|
||||||
return sigHash(header)
|
return sigHash(header)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *XDPoS_v2) Initial(chain consensus.ChainReader, header *types.Header, masternodes []common.Address) error {
|
||||||
|
log.Info("[Initial] initial v2 related parameters")
|
||||||
|
|
||||||
|
if x.highestQuorumCert.ProposedBlockInfo.Round != 0 { //already initialized
|
||||||
|
log.Warn("[Initial] Already initialized")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
x.lock.Lock()
|
||||||
|
defer x.lock.Unlock()
|
||||||
|
// Check header if it is the first consensus v2 block, if so, assign initial values to current round and highestQC
|
||||||
|
|
||||||
|
log.Info("[Initial] highest QC for consensus v2 first block", "Block Num", header.Number.String(), "BlockHash", header.Hash())
|
||||||
|
// Generate new parent blockInfo and put it into QC
|
||||||
|
parentBlockInfo := &utils.BlockInfo{
|
||||||
|
Hash: header.ParentHash,
|
||||||
|
Round: utils.Round(0),
|
||||||
|
Number: big.NewInt(0).Sub(header.Number, big.NewInt(1)),
|
||||||
|
}
|
||||||
|
quorumCert := &utils.QuorumCert{
|
||||||
|
ProposedBlockInfo: parentBlockInfo,
|
||||||
|
Signatures: nil,
|
||||||
|
}
|
||||||
|
x.currentRound = 1
|
||||||
|
x.highestQuorumCert = quorumCert
|
||||||
|
|
||||||
|
// Initial snapshot
|
||||||
|
lastGapNum := header.Number.Uint64() - header.Number.Uint64()%x.config.Epoch - x.config.Gap
|
||||||
|
lastGapHeader := chain.GetHeaderByNumber(lastGapNum)
|
||||||
|
|
||||||
|
snap := newSnapshot(lastGapNum, lastGapHeader.Hash(), x.currentRound, x.highestQuorumCert, masternodes)
|
||||||
|
x.snapshots.Add(snap.Hash, snap)
|
||||||
|
storeSnapshot(snap, x.db)
|
||||||
|
return 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 {
|
||||||
// Verify mined block parent matches highest QC
|
|
||||||
x.lock.Lock()
|
|
||||||
// Check header if it is the first consensus v2 block, if so, assign initial values to current round and highestQC
|
|
||||||
if header.Number.Cmp(big.NewInt(0).Add(x.config.XDPoSV2Block, big.NewInt(1))) == 0 {
|
|
||||||
log.Info("[Prepare] Initilising highest QC for consensus v2 first block", "Block Num", header.Number.String(), "BlockHash", header.Hash())
|
|
||||||
// Generate new parent blockInfo and put it into QC
|
|
||||||
parentBlockInfo := &utils.BlockInfo{
|
|
||||||
Hash: header.ParentHash,
|
|
||||||
Round: utils.Round(0),
|
|
||||||
Number: big.NewInt(0).Sub(header.Number, big.NewInt(1)),
|
|
||||||
}
|
|
||||||
quorumCert := &utils.QuorumCert{
|
|
||||||
ProposedBlockInfo: parentBlockInfo,
|
|
||||||
Signatures: nil,
|
|
||||||
}
|
|
||||||
x.currentRound = 1
|
|
||||||
x.highestQuorumCert = quorumCert
|
|
||||||
}
|
|
||||||
|
|
||||||
|
x.lock.RLock()
|
||||||
currentRound := x.currentRound
|
currentRound := x.currentRound
|
||||||
highestQC := x.highestQuorumCert
|
highestQC := x.highestQuorumCert
|
||||||
x.lock.Unlock()
|
x.lock.RUnlock()
|
||||||
|
|
||||||
if (highestQC == nil) || (header.ParentHash != highestQC.ProposedBlockInfo.Hash) {
|
if (highestQC == nil) || (header.ParentHash != highestQC.ProposedBlockInfo.Hash) {
|
||||||
return consensus.ErrNotReadyToPropose
|
return consensus.ErrNotReadyToPropose
|
||||||
|
|
@ -169,11 +184,11 @@ func (x *XDPoS_v2) Prepare(chain consensus.ChainReader, header *types.Header) er
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if isEpochSwitchBlock {
|
if isEpochSwitchBlock {
|
||||||
snap, err := x.snapshot(chain, number-1, header.ParentHash, nil)
|
snap, err := x.getSnapshot(chain, number-1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
masternodes := snap.GetMasterNodes()
|
masternodes := snap.NextEpochMasterNodes
|
||||||
//TODO: remove penalty nodes and add comeback nodes
|
//TODO: remove penalty nodes and add comeback nodes
|
||||||
for _, v := range masternodes {
|
for _, v := range masternodes {
|
||||||
header.Validators = append(header.Validators, v[:]...)
|
header.Validators = append(header.Validators, v[:]...)
|
||||||
|
|
@ -193,7 +208,7 @@ func (x *XDPoS_v2) Prepare(chain consensus.ChainReader, header *types.Header) er
|
||||||
// Ensure the timestamp has the correct delay
|
// Ensure the timestamp has the correct delay
|
||||||
|
|
||||||
// TODO: Proper deal with time
|
// TODO: Proper deal with time
|
||||||
// TODO: if timestamp > current time, how to deal with future timestamp
|
// TODO: if timestamp > current time, how to deal with future timestamp
|
||||||
header.Time = new(big.Int).Add(parent.Time, new(big.Int).SetUint64(x.config.Period))
|
header.Time = new(big.Int).Add(parent.Time, new(big.Int).SetUint64(x.config.Period))
|
||||||
if header.Time.Int64() < time.Now().Unix() {
|
if header.Time.Int64() < time.Now().Unix() {
|
||||||
header.Time = big.NewInt(time.Now().Unix())
|
header.Time = big.NewInt(time.Now().Unix())
|
||||||
|
|
@ -273,23 +288,17 @@ func (x *XDPoS_v2) Seal(chain consensus.ChainReader, block *types.Block, stop <-
|
||||||
x.signLock.RUnlock()
|
x.signLock.RUnlock()
|
||||||
|
|
||||||
// Bail out if we're unauthorized to sign a block
|
// Bail out if we're unauthorized to sign a block
|
||||||
snap, err := x.snapshot(chain, number-1, header.ParentHash, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
masternodes := x.GetMasternodes(chain, header)
|
masternodes := x.GetMasternodes(chain, header)
|
||||||
if _, authorized := snap.MasterNodes[signer]; !authorized {
|
valid := false
|
||||||
valid := false
|
for _, m := range masternodes {
|
||||||
for _, m := range masternodes {
|
if m == signer {
|
||||||
if m == signer {
|
valid = true
|
||||||
valid = true
|
break
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !valid {
|
|
||||||
return nil, utils.ErrUnauthorized
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if !valid {
|
||||||
|
return nil, utils.ErrUnauthorized
|
||||||
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-stop:
|
case <-stop:
|
||||||
|
|
@ -398,8 +407,8 @@ func (x *XDPoS_v2) IsAuthorisedAddress(chain consensus.ChainReader, header *type
|
||||||
// Copy from v1
|
// Copy from v1
|
||||||
func (x *XDPoS_v2) GetSnapshot(chain consensus.ChainReader, header *types.Header) (*SnapshotV2, error) {
|
func (x *XDPoS_v2) GetSnapshot(chain consensus.ChainReader, header *types.Header) (*SnapshotV2, error) {
|
||||||
number := header.Number.Uint64()
|
number := header.Number.Uint64()
|
||||||
log.Trace("get snapshot", "number", number, "hash", header.Hash())
|
log.Trace("get snapshot", "number", number)
|
||||||
snap, err := x.snapshot(chain, number, header.Hash(), nil)
|
snap, err := x.getSnapshot(chain, number)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -407,84 +416,53 @@ func (x *XDPoS_v2) GetSnapshot(chain consensus.ChainReader, header *types.Header
|
||||||
}
|
}
|
||||||
|
|
||||||
// snapshot retrieves the authorization snapshot at a given point in time.
|
// snapshot retrieves the authorization snapshot at a given point in time.
|
||||||
func (x *XDPoS_v2) snapshot(chain consensus.ChainReader, number uint64, hash common.Hash, parents []*types.Header) (*SnapshotV2, error) {
|
func (x *XDPoS_v2) getSnapshot(chain consensus.ChainReader, number uint64) (*SnapshotV2, error) {
|
||||||
// Search for a SnapshotV2 in memory or on disk for checkpoints
|
// checkpoint snapshot = checkpoint - gap
|
||||||
var (
|
gapBlockNum := number - number%x.config.Epoch - x.config.Gap
|
||||||
headers []*types.Header
|
gapBlockHash := chain.GetHeaderByNumber(gapBlockNum).Hash()
|
||||||
snap *SnapshotV2
|
log.Debug("get snapshot from gap block", "number", gapBlockNum, "hash", gapBlockHash.Hex())
|
||||||
)
|
|
||||||
for snap == nil {
|
// If an in-memory SnapshotV2 was found, use that
|
||||||
// If an in-memory SnapshotV2 was found, use that
|
if s, ok := x.snapshots.Get(gapBlockHash); ok {
|
||||||
if s, ok := x.recents.Get(hash); ok {
|
snap := s.(*SnapshotV2)
|
||||||
snap = s.(*SnapshotV2)
|
log.Trace("Loaded snapshot from memory", "number", gapBlockNum, "hash", gapBlockHash)
|
||||||
break
|
return snap, nil
|
||||||
}
|
|
||||||
// If an on-disk checkpoint snapshot can be found, use that
|
|
||||||
// checkpoint snapshot = checkpoint - gap
|
|
||||||
if (number+x.config.Gap)%x.config.Epoch == 0 {
|
|
||||||
if s, err := loadSnapshot(x.signatures, x.db, hash); err == nil {
|
|
||||||
log.Trace("Loaded snapshot form disk", "number", number, "hash", hash)
|
|
||||||
snap = s
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// If we're at 0 block, make a snapshot
|
|
||||||
// TODO: We may need to store snapshot at the v1 -> v2 switch block
|
|
||||||
if number == 0 {
|
|
||||||
genesis := chain.GetHeaderByNumber(0)
|
|
||||||
if err := x.VerifyHeader(chain, genesis, true); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
signers := make([]common.Address, (len(genesis.Extra)-utils.ExtraVanity-utils.ExtraSeal)/common.AddressLength)
|
|
||||||
for i := 0; i < len(signers); i++ {
|
|
||||||
copy(signers[i][:], genesis.Extra[utils.ExtraVanity+i*common.AddressLength:])
|
|
||||||
}
|
|
||||||
snap = newSnapshot(x.signatures, 0, genesis.Hash(), x.currentRound, x.highestQuorumCert, signers)
|
|
||||||
if err := storeSnapshot(snap, x.db); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
log.Trace("Stored genesis voting snapshot to disk")
|
|
||||||
break
|
|
||||||
}
|
|
||||||
// No snapshot for this header, gather the header and move backward
|
|
||||||
var header *types.Header
|
|
||||||
if len(parents) > 0 {
|
|
||||||
// If we have explicit parents, pick from there (enforced)
|
|
||||||
header = parents[len(parents)-1]
|
|
||||||
if header.Hash() != hash || header.Number.Uint64() != number {
|
|
||||||
return nil, consensus.ErrUnknownAncestor
|
|
||||||
}
|
|
||||||
parents = parents[:len(parents)-1]
|
|
||||||
} else {
|
|
||||||
// No explicit parents (or no more left), reach out to the database
|
|
||||||
header = chain.GetHeader(hash, number)
|
|
||||||
if header == nil {
|
|
||||||
log.Error("[Seal] Failed due to no header found", "hash", hash, "number", number)
|
|
||||||
return nil, consensus.ErrUnknownAncestor
|
|
||||||
}
|
|
||||||
}
|
|
||||||
headers = append(headers, header)
|
|
||||||
number, hash = number-1, header.ParentHash
|
|
||||||
}
|
}
|
||||||
// Previous snapshot found, apply any pending headers on top of it
|
// If an on-disk checkpoint snapshot can be found, use that
|
||||||
for i := 0; i < len(headers)/2; i++ {
|
snap, err := loadSnapshot(x.signatures, x.db, gapBlockHash)
|
||||||
headers[i], headers[len(headers)-1-i] = headers[len(headers)-1-i], headers[i]
|
|
||||||
}
|
|
||||||
snap, err := snap.apply(headers)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Error("Cannot find snapshot from last gap block", "err", err, "number", gapBlockNum, "hash", gapBlockHash)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
x.recents.Add(snap.Hash, snap)
|
|
||||||
|
|
||||||
// If we've generated a new checkpoint snapshot, save to disk
|
log.Trace("Loaded snapshot from disk", "number", gapBlockNum, "hash", gapBlockHash)
|
||||||
// TODO how to save correct snapshot
|
x.snapshots.Add(snap.Hash, snap)
|
||||||
if uint64(snap.Round)%x.config.Epoch == x.config.Gap {
|
return snap, nil
|
||||||
if err = storeSnapshot(snap, x.db); err != nil {
|
}
|
||||||
return nil, err
|
|
||||||
}
|
func (x *XDPoS_v2) UpdateMasternodes(chain consensus.ChainReader, header *types.Header, ms []utils.Masternode) error {
|
||||||
log.Trace("Stored snapshot to disk", "round number", snap.Round, "hash", snap.Hash)
|
number := header.Number.Uint64()
|
||||||
|
log.Trace("take snapshot", "number", number, "hash", header.Hash())
|
||||||
|
|
||||||
|
masterNodes := []common.Address{}
|
||||||
|
for _, m := range ms {
|
||||||
|
masterNodes = append(masterNodes, m.Address)
|
||||||
}
|
}
|
||||||
return snap, err
|
|
||||||
|
x.lock.RLock()
|
||||||
|
snap := newSnapshot(number, header.Hash(), x.currentRound, x.highestQuorumCert, masterNodes)
|
||||||
|
x.lock.RUnlock()
|
||||||
|
|
||||||
|
storeSnapshot(snap, x.db)
|
||||||
|
x.snapshots.Add(snap.Hash, snap)
|
||||||
|
|
||||||
|
nm := []string{}
|
||||||
|
for _, n := range ms {
|
||||||
|
nm = append(nm, n.Address.String())
|
||||||
|
}
|
||||||
|
log.Info("New set of masternodes has been updated to snapshot", "number", snap.Number, "hash", snap.Hash, "new masternodes", nm)
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *XDPoS_v2) VerifyHeader(chain consensus.ChainReader, header *types.Header, fullVerify bool) error {
|
func (x *XDPoS_v2) VerifyHeader(chain consensus.ChainReader, header *types.Header, fullVerify bool) error {
|
||||||
|
|
|
||||||
|
|
@ -2,41 +2,32 @@ package engine_v2
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"sort"
|
|
||||||
|
|
||||||
"github.com/XinFinOrg/XDPoSChain/common"
|
"github.com/XinFinOrg/XDPoSChain/common"
|
||||||
"github.com/XinFinOrg/XDPoSChain/consensus/XDPoS/utils"
|
"github.com/XinFinOrg/XDPoSChain/consensus/XDPoS/utils"
|
||||||
"github.com/XinFinOrg/XDPoSChain/core/types"
|
|
||||||
"github.com/XinFinOrg/XDPoSChain/ethdb"
|
"github.com/XinFinOrg/XDPoSChain/ethdb"
|
||||||
lru "github.com/hashicorp/golang-lru"
|
lru "github.com/hashicorp/golang-lru"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Snapshot is the state of the smart contract validator list
|
// Snapshot is the state of the smart contract validator list
|
||||||
|
// The validator list is used on next epoch master nodes
|
||||||
|
// If we don't have the snapshot, then we have to trace back the gap block smart contract state which is very costly
|
||||||
type SnapshotV2 struct {
|
type SnapshotV2 struct {
|
||||||
sigcache *lru.ARCCache // Cache of recent block signatures to speed up ecrecover
|
|
||||||
|
|
||||||
Round utils.Round `json:"round"` // Round number
|
Round utils.Round `json:"round"` // Round number
|
||||||
Number uint64 `json:"number"` // Block number where the snapshot was created
|
Number uint64 `json:"number"` // Block number where the snapshot was created
|
||||||
Hash common.Hash `json:"hash"` // Block hash where the snapshot was created
|
Hash common.Hash `json:"hash"` // Block hash where the snapshot was created
|
||||||
|
|
||||||
// MasterNodes will get assigned on updateM1
|
// MasterNodes will get assigned on updateM1
|
||||||
MasterNodes map[common.Address]struct{} `json:"masterNodes"` // Set of authorized master nodes at this moment
|
NextEpochMasterNodes []common.Address `json:"masterNodes"` // Set of authorized master nodes at this moment for next epoch
|
||||||
}
|
}
|
||||||
|
|
||||||
// newSnapshot creates a new snapshot with the specified startup parameters. This
|
// create new snapshot for next epoch to use
|
||||||
// method does not initialize the set of recent signers, so only ever use if for
|
func newSnapshot(number uint64, hash common.Hash, round utils.Round, qc *utils.QuorumCert, masternodes []common.Address) *SnapshotV2 {
|
||||||
// the genesis block.
|
|
||||||
func newSnapshot(sigcache *lru.ARCCache, number uint64, hash common.Hash, round utils.Round, qc *utils.QuorumCert, masternodes []common.Address) *SnapshotV2 {
|
|
||||||
snap := &SnapshotV2{
|
snap := &SnapshotV2{
|
||||||
sigcache: sigcache,
|
Round: round,
|
||||||
Round: round,
|
Number: number,
|
||||||
Number: number,
|
Hash: hash,
|
||||||
Hash: hash,
|
NextEpochMasterNodes: masternodes,
|
||||||
|
|
||||||
MasterNodes: make(map[common.Address]struct{}),
|
|
||||||
}
|
|
||||||
for _, signer := range masternodes {
|
|
||||||
snap.MasterNodes[signer] = struct{}{}
|
|
||||||
}
|
}
|
||||||
return snap
|
return snap
|
||||||
}
|
}
|
||||||
|
|
@ -51,7 +42,6 @@ func loadSnapshot(sigcache *lru.ARCCache, db ethdb.Database, hash common.Hash) (
|
||||||
if err := json.Unmarshal(blob, snap); err != nil {
|
if err := json.Unmarshal(blob, snap); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
snap.sigcache = sigcache
|
|
||||||
|
|
||||||
return snap, nil
|
return snap, nil
|
||||||
}
|
}
|
||||||
|
|
@ -65,69 +55,11 @@ func storeSnapshot(s *SnapshotV2, db ethdb.Database) error {
|
||||||
return db.Put(append([]byte("XDPoS-"), s.Hash[:]...), blob)
|
return db.Put(append([]byte("XDPoS-"), s.Hash[:]...), blob)
|
||||||
}
|
}
|
||||||
|
|
||||||
// copy creates a deep copy of the SnapshotV2, though not the individual votes.
|
// retrieves master nodes list in map type
|
||||||
func (s *SnapshotV2) copy() *SnapshotV2 {
|
func (s *SnapshotV2) GetMappedMasterNodes() map[common.Address]struct{} {
|
||||||
cpy := &SnapshotV2{
|
ms := make(map[common.Address]struct{})
|
||||||
sigcache: s.sigcache,
|
for _, n := range s.NextEpochMasterNodes {
|
||||||
Round: s.Round,
|
ms[n] = struct{}{}
|
||||||
Number: s.Number,
|
|
||||||
Hash: s.Hash,
|
|
||||||
MasterNodes: make(map[common.Address]struct{}),
|
|
||||||
}
|
}
|
||||||
for signer := range s.MasterNodes {
|
return ms
|
||||||
cpy.MasterNodes[signer] = struct{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
return cpy
|
|
||||||
}
|
|
||||||
|
|
||||||
// apply creates a new authorization SnapshotV2 by applying the given headers to
|
|
||||||
// the original one.
|
|
||||||
// TODO: XIN-100
|
|
||||||
func (s *SnapshotV2) apply(headers []*types.Header) (*SnapshotV2, error) {
|
|
||||||
return s, nil
|
|
||||||
|
|
||||||
// Allow passing in no headers for cleaner code
|
|
||||||
// if len(headers) == 0 {
|
|
||||||
// return s, nil
|
|
||||||
// }
|
|
||||||
// // Sanity check that the headers can be applied
|
|
||||||
// for i := 0; i < len(headers)-1; i++ {
|
|
||||||
// if headers[i+1].Number.Uint64() != headers[i].Number.Uint64()+1 {
|
|
||||||
// return nil, utils.ErrInvalidHeaderOrder
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// if headers[0].Number.Uint64() != s.Number+1 {
|
|
||||||
// return nil, utils.ErrInvalidChild
|
|
||||||
// }
|
|
||||||
// // Iterate through the headers and create a new SnapshotV2
|
|
||||||
// snap := s.copy()
|
|
||||||
// lastHeader := headers[len(headers)-1]
|
|
||||||
|
|
||||||
// snap.Number += uint64(len(headers))
|
|
||||||
// snap.Hash = lastHeader.Hash()
|
|
||||||
|
|
||||||
// extraV2 := new(utils.ExtraFields_v2)
|
|
||||||
// err := utils.DecodeBytesExtraFields(lastHeader.Extra, &extraV2)
|
|
||||||
// if err != nil {
|
|
||||||
// return nil, err
|
|
||||||
// }
|
|
||||||
// snap.Round = extraV2.Round
|
|
||||||
// return snap, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// signers retrieves the list of authorized signers in ascending order, convert into strings then use native sort lib
|
|
||||||
func (s *SnapshotV2) GetMasterNodes() []common.Address {
|
|
||||||
nodes := make([]common.Address, 0, len(s.MasterNodes))
|
|
||||||
nodeStrs := make([]string, 0, len(s.MasterNodes))
|
|
||||||
|
|
||||||
for node := range s.MasterNodes {
|
|
||||||
nodeStrs = append(nodeStrs, node.Str())
|
|
||||||
}
|
|
||||||
sort.Strings(nodeStrs)
|
|
||||||
for _, str := range nodeStrs {
|
|
||||||
nodes = append(nodes, common.StringToAddress(str))
|
|
||||||
}
|
|
||||||
|
|
||||||
return nodes
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,103 +3,28 @@ package engine_v2
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"math/big"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/XinFinOrg/XDPoSChain/common"
|
"github.com/XinFinOrg/XDPoSChain/common"
|
||||||
"github.com/XinFinOrg/XDPoSChain/consensus/XDPoS/utils"
|
"github.com/XinFinOrg/XDPoSChain/consensus/XDPoS/utils"
|
||||||
"github.com/XinFinOrg/XDPoSChain/core/rawdb"
|
"github.com/XinFinOrg/XDPoSChain/core/rawdb"
|
||||||
"github.com/XinFinOrg/XDPoSChain/core/types"
|
|
||||||
"github.com/XinFinOrg/XDPoSChain/ethdb/leveldb"
|
"github.com/XinFinOrg/XDPoSChain/ethdb/leveldb"
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestGetMasterNodes(t *testing.T) {
|
func TestGetMasterNodes(t *testing.T) {
|
||||||
masterNodes := []common.Address{
|
masterNodes := []common.Address{{0x4}, {0x3}, {0x2}, {0x1}}
|
||||||
{4}, {3}, {2}, {1},
|
snap := newSnapshot(1, common.Hash{}, utils.Round(1), nil, masterNodes)
|
||||||
}
|
|
||||||
snap := newSnapshot(nil, 1, common.Hash{}, utils.Round(1), nil, masterNodes)
|
for _, address := range masterNodes {
|
||||||
sortedNodes := snap.GetMasterNodes()
|
if _, ok := snap.GetMappedMasterNodes()[address]; !ok {
|
||||||
for i := range masterNodes {
|
t.Error("should get master node from map", address.Hex(), snap.GetMappedMasterNodes())
|
||||||
if masterNodes[i] != sortedNodes[3-i] {
|
|
||||||
t.Error("should get sorted master nodes list", i, sortedNodes[i])
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
func TestApplyNewSnapshot(t *testing.T) {
|
|
||||||
t.Skip("apply has been temporary commented out")
|
|
||||||
snap := newSnapshot(nil, 1, common.Hash{}, utils.Round(1), nil, nil)
|
|
||||||
extra := utils.ExtraFields_v2{
|
|
||||||
Round: 10,
|
|
||||||
QuorumCert: &utils.QuorumCert{
|
|
||||||
ProposedBlockInfo: &utils.BlockInfo{},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
extraBytes, err := extra.EncodeToBytes()
|
|
||||||
assert.Nil(t, err)
|
|
||||||
|
|
||||||
headers := []*types.Header{
|
|
||||||
{Number: big.NewInt(2)},
|
|
||||||
{Number: big.NewInt(3)},
|
|
||||||
{Number: big.NewInt(4)},
|
|
||||||
{
|
|
||||||
Number: big.NewInt(5),
|
|
||||||
Extra: extraBytes,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
newSnap, err := snap.apply(headers)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
if newSnap.Number != 5 {
|
|
||||||
t.Error("newSnapshot number should have last header number")
|
|
||||||
}
|
|
||||||
if newSnap.Hash != headers[3].Hash() {
|
|
||||||
t.Error("newSnapshot hash should equal the last header given")
|
|
||||||
}
|
|
||||||
if newSnap.Round != 10 {
|
|
||||||
t.Error("newSnapshot round number should also have last header round number")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestApplyWithWrongHeader(t *testing.T) {
|
|
||||||
t.Skip("apply has been temporary commented out")
|
|
||||||
snap := newSnapshot(nil, 1, common.Hash{}, utils.Round(1), nil, nil)
|
|
||||||
headers := []*types.Header{
|
|
||||||
{Number: big.NewInt(3)},
|
|
||||||
}
|
|
||||||
_, err := snap.apply(headers)
|
|
||||||
assert.Equal(t, err, utils.ErrInvalidChild)
|
|
||||||
|
|
||||||
snap = newSnapshot(nil, 1, common.Hash{}, utils.Round(1), nil, nil)
|
|
||||||
headers = []*types.Header{
|
|
||||||
{Number: big.NewInt(2)},
|
|
||||||
{Number: big.NewInt(4)},
|
|
||||||
}
|
|
||||||
_, err = snap.apply(headers)
|
|
||||||
assert.Equal(t, err, utils.ErrInvalidHeaderOrder)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Should perform deep copy
|
|
||||||
func TestCopySnapshot(t *testing.T) {
|
|
||||||
masterNodes := []common.Address{
|
|
||||||
{4}, {3}, {2}, {1},
|
|
||||||
}
|
|
||||||
snap := newSnapshot(nil, 1, common.Hash{}, utils.Round(1), nil, masterNodes)
|
|
||||||
|
|
||||||
newSnapshot := snap.copy()
|
|
||||||
if newSnapshot == snap {
|
|
||||||
t.Error("should return given different memory address")
|
|
||||||
}
|
|
||||||
|
|
||||||
for node := range snap.MasterNodes {
|
|
||||||
if _, ok := newSnapshot.MasterNodes[node]; !ok {
|
|
||||||
t.Error("snapshot masternodes should copy to new object")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStoreLoadSnapshot(t *testing.T) {
|
func TestStoreLoadSnapshot(t *testing.T) {
|
||||||
snap := newSnapshot(nil, 1, common.Hash{0x1}, utils.Round(1), nil, nil)
|
snap := newSnapshot(1, common.Hash{0x1}, utils.Round(1), nil, nil)
|
||||||
dir, err := ioutil.TempDir("", "snapshot-test")
|
dir, err := ioutil.TempDir("", "snapshot-test")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(fmt.Sprintf("can't create temporary directory: %v", err))
|
panic(fmt.Sprintf("can't create temporary directory: %v", err))
|
||||||
|
|
|
||||||
|
|
@ -602,3 +602,63 @@ func TestVoteShouldNotBeAffectedByFork(t *testing.T) {
|
||||||
t.Fatalf("account 3 should sit in the signer list as previos block result")
|
t.Fatalf("account 3 should sit in the signer list as previos block result")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
V2 Consensus
|
||||||
|
*/
|
||||||
|
/*
|
||||||
|
// Pending for creating cross version blocks
|
||||||
|
func TestV2UpdateSignerListIfVotedBeforeGap(t *testing.T) {
|
||||||
|
config := params.TestXDPoSMockChainConfigWithV2EngineEpochSwitch
|
||||||
|
blockchain, backend, parentBlock, _ := PrepareXDCTestBlockChain(t, int(config.XDPoS.Epoch)+GAP-2, config)
|
||||||
|
// Insert first Block 1349
|
||||||
|
t.Logf("Inserting block with propose at 1349...")
|
||||||
|
blockCoinbaseA := "0xaaa0000000000000000000000000000000001349"
|
||||||
|
tx, err := voteTX(37117, 0, acc1Addr.String())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
//Get from block validator error message
|
||||||
|
merkleRoot := "46234e9cd7e85a267f7f0435b15256a794a2f6d65cc98cdbd21dcd10a01d9772"
|
||||||
|
header := &types.Header{
|
||||||
|
Root: common.HexToHash(merkleRoot),
|
||||||
|
Number: big.NewInt(int64(1349)),
|
||||||
|
ParentHash: parentBlock.Hash(),
|
||||||
|
Coinbase: common.HexToAddress(blockCoinbaseA),
|
||||||
|
}
|
||||||
|
block1349, err := insertBlockTxs(blockchain, header, []*types.Transaction{tx})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
parentBlock = block1349
|
||||||
|
|
||||||
|
// Now, let's mine another block to trigger the GAP block signerList update
|
||||||
|
block1350CoinbaseAddress := "0xaaa0000000000000000000000000000000001350"
|
||||||
|
merkleRoot = "46234e9cd7e85a267f7f0435b15256a794a2f6d65cc98cdbd21dcd10a01d9772"
|
||||||
|
header = &types.Header{
|
||||||
|
Root: common.HexToHash(merkleRoot),
|
||||||
|
Number: big.NewInt(int64(1350)),
|
||||||
|
ParentHash: parentBlock.Hash(),
|
||||||
|
Coinbase: common.HexToAddress(block1350CoinbaseAddress),
|
||||||
|
}
|
||||||
|
block1350, err := insertBlock(blockchain, header)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
signers, err := GetSnapshotSigner(blockchain, block1350.Header())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed while trying to get signers")
|
||||||
|
}
|
||||||
|
// Now, we voted acc 1 to be in the signerList, which will kick out acc3 because it has less funds
|
||||||
|
if signers[acc3Addr.Hex()] == true {
|
||||||
|
debugMessage(backend, signers, t)
|
||||||
|
t.Fatalf("account 3 should NOT sit in the signer list")
|
||||||
|
}
|
||||||
|
if signers[acc1Addr.Hex()] != true {
|
||||||
|
debugMessage(backend, signers, t)
|
||||||
|
t.Fatalf("account 1 should sit in the signer list")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
|
||||||
48
consensus/tests/initialize_v2_test.go
Normal file
48
consensus/tests/initialize_v2_test.go
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
package tests
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/big"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/XinFinOrg/XDPoSChain/common"
|
||||||
|
"github.com/XinFinOrg/XDPoSChain/consensus/XDPoS"
|
||||||
|
"github.com/XinFinOrg/XDPoSChain/consensus/XDPoS/utils"
|
||||||
|
"github.com/XinFinOrg/XDPoSChain/core/types"
|
||||||
|
"github.com/XinFinOrg/XDPoSChain/params"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestYourTurnInitialV2(t *testing.T) {
|
||||||
|
config := params.TestXDPoSMockChainConfigWithV2EngineEpochSwitch
|
||||||
|
blockchain, _, parentBlock, _ := PrepareXDCTestBlockChain(t, int(config.XDPoS.Epoch)-1, config)
|
||||||
|
adaptor := blockchain.Engine().(*XDPoS.XDPoS)
|
||||||
|
|
||||||
|
// Insert block 900
|
||||||
|
t.Logf("Inserting block with propose at 900...")
|
||||||
|
blockCoinbaseA := "0xaaa0000000000000000000000000000000000900"
|
||||||
|
//Get from block validator error message
|
||||||
|
merkleRoot := "35999dded35e8db12de7e6c1471eb9670c162eec616ecebbaf4fddd4676fb930"
|
||||||
|
header := &types.Header{
|
||||||
|
Root: common.HexToHash(merkleRoot),
|
||||||
|
Number: big.NewInt(int64(900)),
|
||||||
|
ParentHash: parentBlock.Hash(),
|
||||||
|
Coinbase: common.HexToAddress(blockCoinbaseA),
|
||||||
|
Extra: common.Hex2Bytes("d7830100018358444388676f312e31352e38856c696e757800000000000000000278c350152e15fa6ffc712a5a73d704ce73e2e103d9e17ae3ff2c6712e44e25b09ac5ee91f6c9ff065551f0dcac6f00cae11192d462db709be3758ccef312ee5eea8d7bad5374c6a652150515d744508b61c1a4deb4e4e7bf057e4e3824c11fd2569bcb77a52905cda63b5a58507910bed335e4c9d87ae0ecdfafd400"),
|
||||||
|
}
|
||||||
|
block900, err := insertBlock(blockchain, header)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// YourTurn is called before mine first v2 block
|
||||||
|
adaptor.YourTurn(blockchain, block900.Header(), common.HexToAddress("xdc0278C350152e15fa6FFC712a5A73D704Ce73E2E1"))
|
||||||
|
assert.Equal(t, adaptor.EngineV2.GetCurrentRound(), utils.Round(1))
|
||||||
|
|
||||||
|
snap, err := adaptor.EngineV2.GetSnapshot(blockchain, block900.Header())
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.NotNil(t, snap)
|
||||||
|
masterNodes := adaptor.EngineV1.GetMasternodesFromCheckpointHeader(block900.Header())
|
||||||
|
for i := 0; i < len(masterNodes); i++ {
|
||||||
|
assert.Equal(t, masterNodes[i].Hex(), snap.NextEpochMasterNodes[i].Hex())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -133,6 +133,8 @@ var (
|
||||||
TestXDPoSMockChainConfig = &ChainConfig{big.NewInt(1337), big.NewInt(0), nil, false, big.NewInt(0), common.Hash{}, big.NewInt(0), big.NewInt(0), big.NewInt(0), nil, new(EthashConfig), nil, &XDPoSConfig{Epoch: 900, Gap: 450, SkipValidation: true, V2: *TestXDPoSV2Config}}
|
TestXDPoSMockChainConfig = &ChainConfig{big.NewInt(1337), big.NewInt(0), nil, false, big.NewInt(0), common.Hash{}, big.NewInt(0), big.NewInt(0), big.NewInt(0), nil, new(EthashConfig), nil, &XDPoSConfig{Epoch: 900, Gap: 450, SkipValidation: true, V2: *TestXDPoSV2Config}}
|
||||||
// XDPoS config with v2 engine after block 10
|
// XDPoS config with v2 engine after block 10
|
||||||
TestXDPoSMockChainConfigWithV2Engine = &ChainConfig{big.NewInt(1337), big.NewInt(0), nil, false, big.NewInt(0), common.Hash{}, big.NewInt(0), big.NewInt(0), big.NewInt(0), nil, new(EthashConfig), nil, &XDPoSConfig{Epoch: 900, Gap: 450, SkipValidation: true, XDPoSV2Block: big.NewInt(10), V2: *TestXDPoSV2Config}}
|
TestXDPoSMockChainConfigWithV2Engine = &ChainConfig{big.NewInt(1337), big.NewInt(0), nil, false, big.NewInt(0), common.Hash{}, big.NewInt(0), big.NewInt(0), big.NewInt(0), nil, new(EthashConfig), nil, &XDPoSConfig{Epoch: 900, Gap: 450, SkipValidation: true, XDPoSV2Block: big.NewInt(10), V2: *TestXDPoSV2Config}}
|
||||||
|
// XDPoS config with v2 engine after block 901
|
||||||
|
TestXDPoSMockChainConfigWithV2EngineEpochSwitch = &ChainConfig{big.NewInt(1337), big.NewInt(0), nil, false, big.NewInt(0), common.Hash{}, big.NewInt(0), big.NewInt(0), big.NewInt(0), nil, new(EthashConfig), nil, &XDPoSConfig{Epoch: 900, Gap: 450, SkipValidation: true, XDPoSV2Block: big.NewInt(900), V2: *TestXDPoSV2Config}}
|
||||||
|
|
||||||
TestChainConfig = &ChainConfig{big.NewInt(1), big.NewInt(0), nil, false, big.NewInt(0), common.Hash{}, big.NewInt(0), big.NewInt(0), big.NewInt(0), nil, new(EthashConfig), nil, 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), nil, new(EthashConfig), nil, nil}
|
||||||
TestRules = TestChainConfig.Rules(new(big.Int))
|
TestRules = TestChainConfig.Rules(new(big.Int))
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue