Check miner data on client block response and block broadcast

This commit is contained in:
DataHoarder 2023-03-09 16:18:42 +01:00
parent 968b07ae0b
commit d59a78c25d
Signed by: DataHoarder
SSH key fingerprint: SHA256:OLTRf6Fl87G52SiR7sWLGNzlJt4WOX+tfI2yxo0z7xk
7 changed files with 230 additions and 150 deletions

View file

@ -2,6 +2,7 @@ package mainchain
import (
"context"
"encoding/hex"
"fmt"
mainblock "git.gammaspectra.live/P2Pool/p2pool-observer/monero/block"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/client"
@ -9,6 +10,7 @@ import (
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/randomx"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/transaction"
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/sidechain"
p2pooltypes "git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/types"
"git.gammaspectra.live/P2Pool/p2pool-observer/types"
"git.gammaspectra.live/P2Pool/p2pool-observer/utils"
"golang.org/x/exp/slices"
@ -31,7 +33,7 @@ type MainChain struct {
mainchainByHash map[types.Hash]*sidechain.ChainMain
tip atomic.Pointer[sidechain.ChainMain]
tipMinerData atomic.Pointer[MinerData]
tipMinerData atomic.Pointer[p2pooltypes.MinerData]
medianTimestamp atomic.Uint64
}
@ -64,15 +66,94 @@ func (c *MainChain) Listen() error {
for {
select {
case <-ctx.Done():
case <-s.ErrC:
return ctx.Err()
case err := <-s.ErrC:
//TODO: retry connection
return err
case fullChainMain := <-s.FullChainMainC:
log.Print(fullChainMain)
if len(fullChainMain.MinerTx.Inputs) < 1 {
continue
}
d := &sidechain.ChainMain{
Difficulty: types.ZeroDifficulty,
Height: fullChainMain.MinerTx.Inputs[0].Gen.Height,
Timestamp: uint64(fullChainMain.Timestamp),
Reward: 0,
Id: types.ZeroHash,
}
for _, o := range fullChainMain.MinerTx.Outputs {
d.Reward += o.Amount
}
outputs := make(transaction.Outputs, 0, len(fullChainMain.MinerTx.Outputs))
var totalReward uint64
for i, o := range fullChainMain.MinerTx.Outputs {
if o.ToKey != nil {
outputs = append(outputs, transaction.Output{
Index: uint64(i),
Reward: o.Amount,
Type: transaction.TxOutToKey,
EphemeralPublicKey: o.ToKey.Key,
ViewTag: 0,
})
} else if o.ToTaggedKey != nil {
tk, _ := hex.DecodeString(o.ToTaggedKey.ViewTag)
outputs = append(outputs, transaction.Output{
Index: uint64(i),
Reward: o.Amount,
Type: transaction.TxOutToTaggedKey,
EphemeralPublicKey: o.ToTaggedKey.Key,
ViewTag: tk[0],
})
} else {
//error
break
}
totalReward += o.Amount
}
if len(outputs) != len(fullChainMain.MinerTx.Outputs) {
continue
}
extraDataRaw, _ := hex.DecodeString(fullChainMain.MinerTx.Extra)
extraTags := transaction.ExtraTags{}
if err = extraTags.UnmarshalBinary(extraDataRaw); err != nil {
continue
}
blockData := &mainblock.Block{
MajorVersion: uint8(fullChainMain.MajorVersion),
MinorVersion: uint8(fullChainMain.MinorVersion),
Timestamp: uint64(fullChainMain.Timestamp),
PreviousId: fullChainMain.PrevID,
Nonce: uint32(fullChainMain.Nonce),
Coinbase: &transaction.CoinbaseTransaction{
Version: uint8(fullChainMain.MinerTx.Version),
UnlockTime: uint64(fullChainMain.MinerTx.UnlockTime),
InputCount: uint8(len(fullChainMain.MinerTx.Inputs)),
InputType: transaction.TxInGen,
GenHeight: fullChainMain.MinerTx.Inputs[0].Gen.Height,
Outputs: outputs,
OutputsBlobSize: 0,
TotalReward: totalReward,
Extra: extraTags,
ExtraBaseRCT: 0,
},
Transactions: fullChainMain.TxHashes,
TransactionParentIndices: nil,
}
c.HandleMainBlock(blockData)
case fullMinerData := <-s.FullMinerDataC:
log.Print(fullMinerData)
c.HandleMinerData(&p2pooltypes.MinerData{
MajorVersion: fullMinerData.MajorVersion,
Height: fullMinerData.Height,
PrevId: fullMinerData.PrevId,
SeedHash: fullMinerData.SeedHash,
Difficulty: fullMinerData.Difficulty,
MedianWeight: fullMinerData.MedianWeight,
AlreadyGeneratedCoins: fullMinerData.AlreadyGeneratedCoins,
MedianTimestamp: fullMinerData.MedianTimestamp,
TimeReceived: time.Now(),
})
}
}
}
@ -249,6 +330,10 @@ func (c *MainChain) GetChainMainTip() *sidechain.ChainMain {
return c.tip.Load()
}
func (c *MainChain) GetMinerDataTip() *p2pooltypes.MinerData {
return c.tipMinerData.Load()
}
func (c *MainChain) updateTip() {
if minerData := c.tipMinerData.Load(); minerData != nil {
if d := c.GetChainMainByHash(minerData.PrevId); d != nil {
@ -336,7 +421,7 @@ func (c *MainChain) DownloadBlockHeaders(currentHeight uint64) error {
return nil
}
func (c *MainChain) HandleMinerData(minerData *MinerData) {
func (c *MainChain) HandleMinerData(minerData *p2pooltypes.MinerData) {
var missingHeights []uint64
func() {
c.lock.Lock()
@ -364,10 +449,6 @@ func (c *MainChain) HandleMinerData(minerData *MinerData) {
} else {
existingPrevMainData.Id = prevMainData.Id
// timestamp and reward is unknown here
existingPrevMainData.Timestamp = 0
existingPrevMainData.Reward = 0
prevMainData = existingPrevMainData
}
@ -429,16 +510,3 @@ func (c *MainChain) getBlockHeader(height uint64) error {
return nil
}
type MinerData struct {
MajorVersion uint8
Height uint64
PrevId types.Hash
SeedHash types.Hash
Difficulty types.Difficulty
MedianWeight uint64
AlreadyGeneratedCoins uint64
MedianTimestamp uint64
//TxBacklog any
TimeReceived time.Time
}

View file

@ -394,14 +394,22 @@ func (c *Client) OnConnection() {
isChainTipBlockRequest := false
if c.chainTipBlockRequest.Swap(false) {
isChainTipBlockRequest = true
//peerHeight := block.Main.Coinbase.GenHeight
//ourHeight :=
if expectedBlockId == types.ZeroHash {
peerHeight := block.Main.Coinbase.GenHeight
ourHeight := c.Owner.MainChain().GetMinerDataTip().Height
if (peerHeight + 2) < ourHeight {
c.Ban(DefaultBanTime, fmt.Errorf("mining on top of a stale block (mainchain peer height %d, expected >= %d)", peerHeight, ourHeight))
return
}
}
//TODO: stale block
log.Printf("[P2PClient] Peer %s tip is at id = %s, height = %d, main height = %d", c.AddressPort.String(), types.HashFromBytes(block.CoinbaseExtra(sidechain.SideTemplateId)), block.Side.Height, block.Main.Coinbase.GenHeight)
if expectedBlockId != types.ZeroHash {
c.Ban(DefaultBanTime, fmt.Errorf("expected block id = %s, got %s", expectedBlockId, types.ZeroHash.String()))
return
}
c.SendPeerListRequest()
@ -469,7 +477,32 @@ func (c *Client) OnConnection() {
//TODO: ban here, but sort blocks properly, maybe a queue to re-try?
return
} else {
//TODO: investigate different monero block mining
ourMinerData := c.Owner.MainChain().GetMinerDataTip()
if block.Main.PreviousId != ourMinerData.PrevId {
// This peer is mining on top of a different Monero block, investigate it
peerHeight := block.Main.Coinbase.GenHeight
ourHeight := ourMinerData.Height
if peerHeight < ourHeight {
if (ourHeight - peerHeight) < 5 {
elapsedTime := time.Now().Sub(ourMinerData.TimeReceived)
if (ourHeight-peerHeight) > 1 || elapsedTime > (time.Second*10) {
log.Printf("[P2PClient] Peer %s broadcasted a stale block (%d ms late, mainchain height %d, expected >= %d), ignoring it", c.AddressPort.String(), elapsedTime.Milliseconds(), peerHeight, ourHeight)
}
} else {
c.Ban(DefaultBanTime, fmt.Errorf("broadcasted an unreasonably stale block (mainchain height %d, expected >= %d)", peerHeight, ourHeight))
return
}
} else if peerHeight > ourHeight {
if peerHeight >= (ourHeight + 2) {
log.Printf("[P2PClient] Peer %s is ahead on mainchain (mainchain height %d, your height %d). Is monerod stuck or lagging?", c.AddressPort.String(), peerHeight, ourHeight)
}
} else {
log.Printf("[P2PClient] Peer %s is mining on an alternative mainchain tip (mainchain height %d, previous_id = %s)", c.AddressPort.String(), peerHeight, block.Main.PreviousId)
}
}
block.WantBroadcast.Store(true)
if missingBlocks, err = c.Owner.SideChain().AddPoolBlockExternal(block); err != nil {

View file

@ -5,7 +5,9 @@ import (
"crypto/rand"
"encoding/binary"
"errors"
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/mainchain"
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/sidechain"
p2pooltypes "git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/types"
"git.gammaspectra.live/P2Pool/p2pool-observer/utils"
"golang.org/x/exp/slices"
"log"
@ -18,8 +20,16 @@ import (
"unsafe"
)
type P2PoolInterface interface {
sidechain.ConsensusProvider
SideChain() *sidechain.SideChain
MainChain() *mainchain.MainChain
GetChainMainTip() *sidechain.ChainMain
GetMinerDataTip() *p2pooltypes.MinerData
}
type Server struct {
sidechain *sidechain.SideChain
p2pool P2PoolInterface
peerId uint64
versionInformation PeerVersionInformation
@ -47,7 +57,7 @@ type Server struct {
ctx context.Context
}
func NewServer(sidechain *sidechain.SideChain, listenAddress string, externalListenPort uint16, maxOutgoingPeers, maxIncomingPeers uint32, ctx context.Context) (*Server, error) {
func NewServer(p2pool P2PoolInterface, listenAddress string, externalListenPort uint16, maxOutgoingPeers, maxIncomingPeers uint32, ctx context.Context) (*Server, error) {
peerId := make([]byte, int(unsafe.Sizeof(uint64(0))))
_, err := rand.Read(peerId)
if err != nil {
@ -60,7 +70,7 @@ func NewServer(sidechain *sidechain.SideChain, listenAddress string, externalLis
}
s := &Server{
sidechain: sidechain,
p2pool: p2pool,
listenAddress: addrPort,
externalListenPort: externalListenPort,
peerId: binary.LittleEndian.Uint64(peerId),
@ -316,7 +326,11 @@ func (s *Server) PeerId() uint64 {
}
func (s *Server) SideChain() *sidechain.SideChain {
return s.sidechain
return s.p2pool.SideChain()
}
func (s *Server) MainChain() *mainchain.MainChain {
return s.p2pool.MainChain()
}
func (s *Server) updateClients() {
@ -384,5 +398,5 @@ func (s *Server) Broadcast(block *sidechain.PoolBlock) {
}
func (s *Server) Consensus() *sidechain.Consensus {
return s.sidechain.Consensus()
return s.p2pool.Consensus()
}

View file

@ -3,7 +3,6 @@ package p2pool
import (
"context"
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/block"
@ -13,6 +12,7 @@ import (
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/mainchain"
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/p2p"
"git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/sidechain"
p2pooltypes "git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/types"
"git.gammaspectra.live/P2Pool/p2pool-observer/types"
"log"
"strconv"
@ -95,7 +95,7 @@ func NewP2Pool(consensus *sidechain.Consensus, settings map[string]string) (*P2P
externalListenPort, _ = strconv.ParseUint(externalPort, 10, 0)
}
if pool.server, err = p2p.NewServer(pool.sidechain, listenAddress, uint16(externalListenPort), uint32(maxOutgoingPeers), uint32(maxIncomingPeers), pool.ctx); err != nil {
if pool.server, err = p2p.NewServer(pool, listenAddress, uint16(externalListenPort), uint32(maxOutgoingPeers), uint32(maxIncomingPeers), pool.ctx); err != nil {
return nil, err
}
@ -114,8 +114,13 @@ func (p *P2Pool) GetChainMainTip() *sidechain.ChainMain {
return p.mainchain.GetChainMainTip()
}
func (p *P2Pool) GetMinerDataTip() *p2pooltypes.MinerData {
return p.mainchain.GetMinerDataTip()
}
// GetMinimalBlockHeaderByHeight Only Id / Height / Timestamp are assured
func (p *P2Pool) GetMinimalBlockHeaderByHeight(height uint64) *block.Header {
lowerThanTip := height <= p.mainchain.GetChainMainTip().Height
if chainMain := p.mainchain.GetChainMainByHeight(height); chainMain != nil && chainMain.Id != types.ZeroHash {
return &block.Header{
Timestamp: chainMain.Timestamp,
@ -124,7 +129,7 @@ func (p *P2Pool) GetMinimalBlockHeaderByHeight(height uint64) *block.Header {
Difficulty: chainMain.Difficulty,
Id: chainMain.Id,
}
} else {
} else if lowerThanTip {
if header, err := p.ClientRPC().GetBlockHeaderByHeight(height, p.ctx); err != nil {
return nil
} else {
@ -146,11 +151,14 @@ func (p *P2Pool) GetMinimalBlockHeaderByHeight(height uint64) *block.Header {
return blockHeader
}
}
return nil
}
func (p *P2Pool) GetDifficultyByHeight(height uint64) types.Difficulty {
lowerThanTip := height <= p.mainchain.GetChainMainTip().Height
if chainMain := p.mainchain.GetChainMainByHeight(height); chainMain != nil && chainMain.Difficulty != types.ZeroDifficulty {
return chainMain.Difficulty
} else {
} else if lowerThanTip {
//TODO cache
if header, err := p.ClientRPC().GetBlockHeaderByHeight(height, p.ctx); err != nil {
return types.ZeroDifficulty
@ -173,6 +181,8 @@ func (p *P2Pool) GetDifficultyByHeight(height uint64) types.Difficulty {
return blockHeader.Difficulty
}
}
return types.ZeroDifficulty
}
// GetMinimalBlockHeaderByHash Only Id / Height / Timestamp are assured
@ -221,6 +231,14 @@ func (p *P2Pool) Context() context.Context {
return p.ctx
}
func (p *P2Pool) SideChain() *sidechain.SideChain {
return p.sidechain
}
func (p *P2Pool) MainChain() *mainchain.MainChain {
return p.mainchain
}
func (p *P2Pool) Server() *p2p.Server {
return p.server
}
@ -241,106 +259,11 @@ func (p *P2Pool) Run() (err error) {
return err
}
if zmqStream, err := p.ClientZMQ().Listen(p.ctx); err != nil {
return err
} else {
go func() {
for {
select {
case <-zmqStream.ErrC:
p.Close()
return
case fullChainMain := <-zmqStream.FullChainMainC:
if len(fullChainMain.MinerTx.Inputs) < 1 {
continue
}
d := &sidechain.ChainMain{
Difficulty: types.ZeroDifficulty,
Height: fullChainMain.MinerTx.Inputs[0].Gen.Height,
Timestamp: uint64(fullChainMain.Timestamp),
Reward: 0,
Id: types.ZeroHash,
}
for _, o := range fullChainMain.MinerTx.Outputs {
d.Reward += o.Amount
}
outputs := make(transaction.Outputs, 0, len(fullChainMain.MinerTx.Outputs))
var totalReward uint64
for i, o := range fullChainMain.MinerTx.Outputs {
if o.ToKey != nil {
outputs = append(outputs, transaction.Output{
Index: uint64(i),
Reward: o.Amount,
Type: transaction.TxOutToKey,
EphemeralPublicKey: o.ToKey.Key,
ViewTag: 0,
})
} else if o.ToTaggedKey != nil {
tk, _ := hex.DecodeString(o.ToTaggedKey.ViewTag)
outputs = append(outputs, transaction.Output{
Index: uint64(i),
Reward: o.Amount,
Type: transaction.TxOutToTaggedKey,
EphemeralPublicKey: o.ToTaggedKey.Key,
ViewTag: tk[0],
})
} else {
//error
break
}
totalReward += o.Amount
}
if len(outputs) != len(fullChainMain.MinerTx.Outputs) {
continue
}
extraDataRaw, _ := hex.DecodeString(fullChainMain.MinerTx.Extra)
extraTags := transaction.ExtraTags{}
if err = extraTags.UnmarshalBinary(extraDataRaw); err != nil {
continue
}
blockData := &block.Block{
MajorVersion: uint8(fullChainMain.MajorVersion),
MinorVersion: uint8(fullChainMain.MinorVersion),
Timestamp: uint64(fullChainMain.Timestamp),
PreviousId: fullChainMain.PrevID,
Nonce: uint32(fullChainMain.Nonce),
Coinbase: &transaction.CoinbaseTransaction{
Version: uint8(fullChainMain.MinerTx.Version),
UnlockTime: uint64(fullChainMain.MinerTx.UnlockTime),
InputCount: uint8(len(fullChainMain.MinerTx.Inputs)),
InputType: transaction.TxInGen,
GenHeight: fullChainMain.MinerTx.Inputs[0].Gen.Height,
Outputs: outputs,
OutputsBlobSize: 0,
TotalReward: totalReward,
Extra: extraTags,
ExtraBaseRCT: 0,
},
Transactions: fullChainMain.TxHashes,
TransactionParentIndices: nil,
}
p.mainchain.HandleMainBlock(blockData)
case fullMinerData := <-zmqStream.FullMinerDataC:
p.mainchain.HandleMinerData(&mainchain.MinerData{
MajorVersion: fullMinerData.MajorVersion,
Height: fullMinerData.Height,
PrevId: fullMinerData.PrevId,
SeedHash: fullMinerData.SeedHash,
Difficulty: fullMinerData.Difficulty,
MedianWeight: fullMinerData.MedianWeight,
AlreadyGeneratedCoins: fullMinerData.AlreadyGeneratedCoins,
MedianTimestamp: fullMinerData.MedianTimestamp,
TimeReceived: time.Now(),
})
}
}
}()
}
go func() {
if p.mainchain.Listen() != nil {
p.Close()
}
}()
p.started.Store(true)
@ -405,7 +328,7 @@ func (p *P2Pool) getMinerData() error {
prevId, _ := types.HashFromString(minerData.PrevId)
seedHash, _ := types.HashFromString(minerData.SeedHash)
diff, _ := types.DifficultyFromString(minerData.Difficulty)
p.mainchain.HandleMinerData(&mainchain.MinerData{
p.mainchain.HandleMinerData(&p2pooltypes.MinerData{
MajorVersion: minerData.MajorVersion,
Height: minerData.Height,
PrevId: prevId,
@ -463,5 +386,16 @@ func (p *P2Pool) Started() bool {
}
func (p *P2Pool) Broadcast(block *sidechain.PoolBlock) {
minerData := p.GetMinerDataTip()
if (block.Main.Coinbase.GenHeight)+2 < minerData.Height {
log.Printf("[P2Pool] Trying to broadcast a stale block %s (mainchain height %d, current height is %d)", block.SideTemplateId(p.consensus), block.Main.Coinbase.GenHeight, minerData.Height)
return
}
if block.Main.Coinbase.GenHeight > (minerData.Height + 2) {
log.Printf("[P2Pool] Trying to broadcast a block %s ahead on mainchain (mainchain height %d, current height is %d)", block.SideTemplateId(p.consensus), block.Main.Coinbase.GenHeight, minerData.Height)
return
}
p.server.Broadcast(block)
}

View file

@ -13,10 +13,10 @@ import (
const BlockSaveEpochSize = 32
const (
BlockSaveOptionTemplate = 1 << 0
//BlockSaveOptionDeterministicPrivateKey = 1 << 1
BlockSaveOptionDeterministicBlobs = 1 << 2
BlockSaveOptionUncles = 1 << 3
BlockSaveOptionTemplate = 1 << 0
BlockSaveOptionDeterministicPrivateKeySeed = 1 << 1
BlockSaveOptionDeterministicBlobs = 1 << 2
BlockSaveOptionUncles = 1 << 3
BlockSaveFieldSizeInBits = 8
@ -60,9 +60,6 @@ func (c *SideChain) saveBlock(block *PoolBlock) {
c.sidechainLock.RLock()
defer c.sidechainLock.RUnlock()
//only store keys when not deterministic
//isDeterministicPrivateKey := c.isPoolBlockTransactionKeyIsDeterministic(block)
calculatedOutputs := c.calculateOutputs(block)
calcBlob, _ := calculatedOutputs.MarshalBinary()
blockBlob, _ := block.Main.Coinbase.Outputs.MarshalBinary()
@ -76,13 +73,26 @@ func (c *SideChain) saveBlock(block *PoolBlock) {
parent := c.getParent(block)
//only store keys when not deterministic
isDeterministicPrivateKeySeed := parent != nil && c.isPoolBlockTransactionKeyIsDeterministic(block)
if isDeterministicPrivateKeySeed && block.ShareVersion() > ShareVersion_V1 {
expectedSeed := parent.Side.CoinbasePrivateKeySeed
if parent.Main.PreviousId != block.Main.PreviousId {
expectedSeed = parent.CalculateTransactionPrivateKeySeed()
}
if block.Side.CoinbasePrivateKeySeed != expectedSeed {
isDeterministicPrivateKeySeed = false
}
}
blob := make([]byte, 0, 4096*2)
var blockFlags uint64
/*if isDeterministicPrivateKey {
blockFlags |= BlockSaveOptionDeterministicPrivateKey
}*/
if isDeterministicPrivateKeySeed {
blockFlags |= BlockSaveOptionDeterministicPrivateKeySeed
}
if !storeBlob {
blockFlags |= BlockSaveOptionDeterministicBlobs
@ -163,8 +173,8 @@ func (c *SideChain) saveBlock(block *PoolBlock) {
blob = binary.AppendUvarint(blob, minerAddressOffset)
}
// private key, if needed
if true /*(blockFlags & BlockSaveOptionDeterministicPrivateKey) == 0*/ {
// private key seed, if needed
if (blockFlags&BlockSaveOptionDeterministicPrivateKeySeed) == 0 || (block.ShareVersion() > ShareVersion_V1 && (blockFlags&BlockSaveOptionTemplate) != 0) {
blob = append(blob, block.Side.CoinbasePrivateKeySeed[:]...)
//public may be needed on invalid - TODO check
//blob = append(blob, block.CoinbaseExtra(SideCoinbasePublicKey)...)

View file

@ -12,6 +12,7 @@ import (
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/crypto"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/randomx"
"git.gammaspectra.live/P2Pool/p2pool-observer/monero/transaction"
p2pooltypes "git.gammaspectra.live/P2Pool/p2pool-observer/p2pool/types"
"git.gammaspectra.live/P2Pool/p2pool-observer/types"
"git.gammaspectra.live/P2Pool/p2pool-observer/utils"
"golang.org/x/exp/slices"
@ -42,6 +43,7 @@ type P2PoolInterface interface {
UpdateBlockFound(data *ChainMain, block *PoolBlock)
SubmitBlock(block *mainblock.Block)
GetChainMainTip() *ChainMain
GetMinerDataTip() *p2pooltypes.MinerData
}
type ChainMain struct {

19
p2pool/types/minerdata.go Normal file
View file

@ -0,0 +1,19 @@
package types
import (
"git.gammaspectra.live/P2Pool/p2pool-observer/types"
"time"
)
type MinerData struct {
MajorVersion uint8
Height uint64
PrevId types.Hash
SeedHash types.Hash
Difficulty types.Difficulty
MedianWeight uint64
AlreadyGeneratedCoins uint64
MedianTimestamp uint64
//TxBacklog any
TimeReceived time.Time
}