package types import ( "bytes" "fmt" "strings" "sync" "time" "github.com/pkg/errors" "github.com/tendermint/tendermint/crypto" "github.com/tendermint/tendermint/crypto/merkle" "github.com/tendermint/tendermint/crypto/tmhash" cmn "github.com/tendermint/tendermint/libs/common" "github.com/tendermint/tendermint/version" ) const ( // MaxHeaderBytes is a maximum header size (including amino overhead). MaxHeaderBytes int64 = 653 // MaxAminoOverheadForBlock - maximum amino overhead to encode a block (up to // MaxBlockSizeBytes in size) not including it's parts except Data. // This means it also excludes the overhead for individual transactions. // To compute individual transactions' overhead use types.ComputeAminoOverhead(tx types.Tx, fieldNum int). // // Uvarint length of MaxBlockSizeBytes: 4 bytes // 2 fields (2 embedded): 2 bytes // Uvarint length of Data.Txs: 4 bytes // Data.Txs field: 1 byte MaxAminoOverheadForBlock int64 = 11 ) // Block defines the atomic unit of a Tendermint blockchain. type Block struct { mtx sync.Mutex Header `json:"header"` Data `json:"data"` Evidence EvidenceData `json:"evidence"` LastCommit *Commit `json:"last_commit"` } // MakeBlock returns a new block with an empty header, except what can be // computed from itself. // It populates the same set of fields validated by ValidateBasic. func MakeBlock(height int64, txs []Tx, lastCommit *Commit, evidence []Evidence) *Block { block := &Block{ Header: Header{ Height: height, NumTxs: int64(len(txs)), }, Data: Data{ Txs: txs, }, Evidence: EvidenceData{Evidence: evidence}, LastCommit: lastCommit, } block.fillHeader() return block } // ValidateBasic performs basic validation that doesn't involve state data. // It checks the internal consistency of the block. // Further validation is done using state#ValidateBlock. func (b *Block) ValidateBasic() error { if b == nil { return errors.New("nil block") } b.mtx.Lock() defer b.mtx.Unlock() if len(b.ChainID) > MaxChainIDLen { return fmt.Errorf("ChainID is too long. Max is %d, got %d", MaxChainIDLen, len(b.ChainID)) } if b.Height < 0 { return errors.New("Negative Header.Height") } else if b.Height == 0 { return errors.New("Zero Header.Height") } // NOTE: Timestamp validation is subtle and handled elsewhere. newTxs := int64(len(b.Data.Txs)) if b.NumTxs != newTxs { return fmt.Errorf("Wrong Header.NumTxs. Expected %v, got %v", newTxs, b.NumTxs, ) } // TODO: fix tests so we can do this /*if b.TotalTxs < b.NumTxs { return fmt.Errorf("Header.TotalTxs (%d) is less than Header.NumTxs (%d)", b.TotalTxs, b.NumTxs) }*/ if b.TotalTxs < 0 { return errors.New("Negative Header.TotalTxs") } if err := b.LastBlockID.ValidateBasic(); err != nil { return fmt.Errorf("Wrong Header.LastBlockID: %v", err) } // Validate the last commit and its hash. if b.Header.Height > 1 { if b.LastCommit == nil { return errors.New("nil LastCommit") } if err := b.LastCommit.ValidateBasic(); err != nil { return fmt.Errorf("Wrong LastCommit") } } if err := ValidateHash(b.LastCommitHash); err != nil { return fmt.Errorf("Wrong Header.LastCommitHash: %v", err) } if !bytes.Equal(b.LastCommitHash, b.LastCommit.Hash()) { return fmt.Errorf("Wrong Header.LastCommitHash. Expected %v, got %v", b.LastCommit.Hash(), b.LastCommitHash, ) } // Validate the hash of the transactions. // NOTE: b.Data.Txs may be nil, but b.Data.Hash() // still works fine if err := ValidateHash(b.DataHash); err != nil { return fmt.Errorf("Wrong Header.DataHash: %v", err) } if !bytes.Equal(b.DataHash, b.Data.Hash()) { return fmt.Errorf( "Wrong Header.DataHash. Expected %v, got %v", b.Data.Hash(), b.DataHash, ) } // Basic validation of hashes related to application data. // Will validate fully against state in state#ValidateBlock. if err := ValidateHash(b.ValidatorsHash); err != nil { return fmt.Errorf("Wrong Header.ValidatorsHash: %v", err) } if err := ValidateHash(b.NextValidatorsHash); err != nil { return fmt.Errorf("Wrong Header.NextValidatorsHash: %v", err) } if err := ValidateHash(b.ConsensusHash); err != nil { return fmt.Errorf("Wrong Header.ConsensusHash: %v", err) } // NOTE: AppHash is arbitrary length if err := ValidateHash(b.LastResultsHash); err != nil { return fmt.Errorf("Wrong Header.LastResultsHash: %v", err) } // Validate evidence and its hash. if err := ValidateHash(b.EvidenceHash); err != nil { return fmt.Errorf("Wrong Header.EvidenceHash: %v", err) } // NOTE: b.Evidence.Evidence may be nil, but we're just looping. for i, ev := range b.Evidence.Evidence { if err := ev.ValidateBasic(); err != nil { return fmt.Errorf("Invalid evidence (#%d): %v", i, err) } } if !bytes.Equal(b.EvidenceHash, b.Evidence.Hash()) { return fmt.Errorf("Wrong Header.EvidenceHash. Expected %v, got %v", b.EvidenceHash, b.Evidence.Hash(), ) } if len(b.ProposerAddress) != crypto.AddressSize { return fmt.Errorf("Expected len(Header.ProposerAddress) to be %d, got %d", crypto.AddressSize, len(b.ProposerAddress)) } return nil } // fillHeader fills in any remaining header fields that are a function of the block data func (b *Block) fillHeader() { if b.LastCommitHash == nil { b.LastCommitHash = b.LastCommit.Hash() } if b.DataHash == nil { b.DataHash = b.Data.Hash() } if b.EvidenceHash == nil { b.EvidenceHash = b.Evidence.Hash() } } // Hash computes and returns the block hash. // If the block is incomplete, block hash is nil for safety. func (b *Block) Hash() cmn.HexBytes { if b == nil { return nil } b.mtx.Lock() defer b.mtx.Unlock() if b == nil || b.LastCommit == nil { return nil } b.fillHeader() return b.Header.Hash() } // MakePartSet returns a PartSet containing parts of a serialized block. // This is the form in which the block is gossipped to peers. // CONTRACT: partSize is greater than zero. func (b *Block) MakePartSet(partSize int) *PartSet { if b == nil { return nil } b.mtx.Lock() defer b.mtx.Unlock() // We prefix the byte length, so that unmarshaling // can easily happen via a reader. bz, err := cdc.MarshalBinaryLengthPrefixed(b) if err != nil { panic(err) } return NewPartSetFromData(bz, partSize) } // HashesTo is a convenience function that checks if a block hashes to the given argument. // Returns false if the block is nil or the hash is empty. func (b *Block) HashesTo(hash []byte) bool { if len(hash) == 0 { return false } if b == nil { return false } return bytes.Equal(b.Hash(), hash) } // Size returns size of the block in bytes. func (b *Block) Size() int { bz, err := cdc.MarshalBinaryBare(b) if err != nil { return 0 } return len(bz) } // String returns a string representation of the block func (b *Block) String() string { return b.StringIndented("") } // StringIndented returns a string representation of the block func (b *Block) StringIndented(indent string) string { if b == nil { return "nil-Block" } return fmt.Sprintf(`Block{ %s %v %s %v %s %v %s %v %s}#%v`, indent, b.Header.StringIndented(indent+" "), indent, b.Data.StringIndented(indent+" "), indent, b.Evidence.StringIndented(indent+" "), indent, b.LastCommit.StringIndented(indent+" "), indent, b.Hash()) } // StringShort returns a shortened string representation of the block func (b *Block) StringShort() string { if b == nil { return "nil-Block" } return fmt.Sprintf("Block#%v", b.Hash()) } //----------------------------------------------------------- // These methods are for Protobuf Compatibility // Marshal returns the amino encoding. func (b *Block) Marshal() ([]byte, error) { return cdc.MarshalBinaryBare(b) } // MarshalTo calls Marshal and copies to the given buffer. func (b *Block) MarshalTo(data []byte) (int, error) { bs, err := b.Marshal() if err != nil { return -1, err } return copy(data, bs), nil } // Unmarshal deserializes from amino encoded form. func (b *Block) Unmarshal(bs []byte) error { return cdc.UnmarshalBinaryBare(bs, b) } //----------------------------------------------------------------------------- // MaxDataBytes returns the maximum size of block's data. // // XXX: Panics on negative result. func MaxDataBytes(maxBytes int64, valsCount, evidenceCount int) int64 { maxDataBytes := maxBytes - MaxAminoOverheadForBlock - MaxHeaderBytes - int64(valsCount)*MaxVoteBytes - int64(evidenceCount)*MaxEvidenceBytes if maxDataBytes < 0 { panic(fmt.Sprintf( "Negative MaxDataBytes. Block.MaxBytes=%d is too small to accommodate header&lastCommit&evidence=%d", maxBytes, -(maxDataBytes - maxBytes), )) } return maxDataBytes } // MaxDataBytesUnknownEvidence returns the maximum size of block's data when // evidence count is unknown. MaxEvidencePerBlock will be used for the size // of evidence. // // XXX: Panics on negative result. func MaxDataBytesUnknownEvidence(maxBytes int64, valsCount int) int64 { _, maxEvidenceBytes := MaxEvidencePerBlock(maxBytes) maxDataBytes := maxBytes - MaxAminoOverheadForBlock - MaxHeaderBytes - int64(valsCount)*MaxVoteBytes - maxEvidenceBytes if maxDataBytes < 0 { panic(fmt.Sprintf( "Negative MaxDataBytesUnknownEvidence. Block.MaxBytes=%d is too small to accommodate header&lastCommit&evidence=%d", maxBytes, -(maxDataBytes - maxBytes), )) } return maxDataBytes } //----------------------------------------------------------------------------- // Header defines the structure of a Tendermint block header. // NOTE: changes to the Header should be duplicated in: // - header.Hash() // - abci.Header // - /docs/spec/blockchain/blockchain.md type Header struct { // basic block info Version version.Consensus `json:"version"` ChainID string `json:"chain_id"` Height int64 `json:"height"` Time time.Time `json:"time"` NumTxs int64 `json:"num_txs"` TotalTxs int64 `json:"total_txs"` // prev block info LastBlockID BlockID `json:"last_block_id"` // hashes of block data LastCommitHash cmn.HexBytes `json:"last_commit_hash"` // commit from validators from the last block DataHash cmn.HexBytes `json:"data_hash"` // transactions // hashes from the app output from the prev block ValidatorsHash cmn.HexBytes `json:"validators_hash"` // validators for the current block NextValidatorsHash cmn.HexBytes `json:"next_validators_hash"` // validators for the next block ConsensusHash cmn.HexBytes `json:"consensus_hash"` // consensus params for current block AppHash cmn.HexBytes `json:"app_hash"` // state after txs from the previous block LastResultsHash cmn.HexBytes `json:"last_results_hash"` // root hash of all results from the txs from the previous block // consensus info EvidenceHash cmn.HexBytes `json:"evidence_hash"` // evidence included in the block ProposerAddress Address `json:"proposer_address"` // original proposer of the block } // Populate the Header with state-derived data. // Call this after MakeBlock to complete the Header. func (h *Header) Populate( version version.Consensus, chainID string, timestamp time.Time, lastBlockID BlockID, totalTxs int64, valHash, nextValHash []byte, consensusHash, appHash, lastResultsHash []byte, proposerAddress Address, ) { h.Version = version h.ChainID = chainID h.Time = timestamp h.LastBlockID = lastBlockID h.TotalTxs = totalTxs h.ValidatorsHash = valHash h.NextValidatorsHash = nextValHash h.ConsensusHash = consensusHash h.AppHash = appHash h.LastResultsHash = lastResultsHash h.ProposerAddress = proposerAddress } // Hash returns the hash of the header. // It computes a Merkle tree from the header fields // ordered as they appear in the Header. // Returns nil if ValidatorHash is missing, // since a Header is not valid unless there is // a ValidatorsHash (corresponding to the validator set). func (h *Header) Hash() cmn.HexBytes { if h == nil || len(h.ValidatorsHash) == 0 { return nil } return merkle.SimpleHashFromByteSlices([][]byte{ cdcEncode(h.Version), cdcEncode(h.ChainID), cdcEncode(h.Height), cdcEncode(h.Time), cdcEncode(h.NumTxs), cdcEncode(h.TotalTxs), cdcEncode(h.LastBlockID), cdcEncode(h.LastCommitHash), cdcEncode(h.DataHash), cdcEncode(h.ValidatorsHash), cdcEncode(h.NextValidatorsHash), cdcEncode(h.ConsensusHash), cdcEncode(h.AppHash), cdcEncode(h.LastResultsHash), cdcEncode(h.EvidenceHash), cdcEncode(h.ProposerAddress), }) } // StringIndented returns a string representation of the header func (h *Header) StringIndented(indent string) string { if h == nil { return "nil-Header" } return fmt.Sprintf(`Header{ %s Version: %v %s ChainID: %v %s Height: %v %s Time: %v %s NumTxs: %v %s TotalTxs: %v %s LastBlockID: %v %s LastCommit: %v %s Data: %v %s Validators: %v %s NextValidators: %v %s App: %v %s Consensus: %v %s Results: %v %s Evidence: %v %s Proposer: %v %s}#%v`, indent, h.Version, indent, h.ChainID, indent, h.Height, indent, h.Time, indent, h.NumTxs, indent, h.TotalTxs, indent, h.LastBlockID, indent, h.LastCommitHash, indent, h.DataHash, indent, h.ValidatorsHash, indent, h.NextValidatorsHash, indent, h.AppHash, indent, h.ConsensusHash, indent, h.LastResultsHash, indent, h.EvidenceHash, indent, h.ProposerAddress, indent, h.Hash()) } //------------------------------------- // CommitSig is a vote included in a Commit. // For now, it is identical to a vote, // but in the future it will contain fewer fields // to eliminate the redundancy in commits. // See https://github.com/tendermint/tendermint/issues/1648. type CommitSig Vote // String returns the underlying Vote.String() func (cs *CommitSig) String() string { return cs.toVote().String() } // toVote converts the CommitSig to a vote. // TODO: deprecate for #1648. Converting to Vote will require // access to ValidatorSet. func (cs *CommitSig) toVote() *Vote { if cs == nil { return nil } v := Vote(*cs) return &v } // Commit contains the evidence that a block was committed by a set of validators. // NOTE: Commit is empty for height 1, but never nil. type Commit struct { // NOTE: The Precommits are in order of address to preserve the bonded ValidatorSet order. // Any peer with a block can gossip precommits by index with a peer without recalculating the // active ValidatorSet. BlockID BlockID `json:"block_id"` Precommits []*CommitSig `json:"precommits"` // memoized in first call to corresponding method // NOTE: can't memoize in constructor because constructor // isn't used for unmarshaling height int64 round int hash cmn.HexBytes bitArray *cmn.BitArray } // NewCommit returns a new Commit with the given blockID and precommits. // TODO: memoize ValidatorSet in constructor so votes can be easily reconstructed // from CommitSig after #1648. func NewCommit(blockID BlockID, precommits []*CommitSig) *Commit { return &Commit{ BlockID: blockID, Precommits: precommits, } } // VoteSignBytes constructs the SignBytes for the given CommitSig. // The only unique part of the SignBytes is the Timestamp - all other fields // signed over are otherwise the same for all validators. func (commit *Commit) VoteSignBytes(chainID string, cs *CommitSig) []byte { return commit.ToVote(cs).SignBytes(chainID) } // memoizeHeightRound memoizes the height and round of the commit using // the first non-nil vote. func (commit *Commit) memoizeHeightRound() { if len(commit.Precommits) == 0 { return } if commit.height > 0 { return } for _, precommit := range commit.Precommits { if precommit != nil { commit.height = precommit.Height commit.round = precommit.Round return } } } // ToVote converts a CommitSig to a Vote. // If the CommitSig is nil, the Vote will be nil. func (commit *Commit) ToVote(cs *CommitSig) *Vote { // TODO: use commit.validatorSet to reconstruct vote // and deprecate .toVote return cs.toVote() } // Height returns the height of the commit func (commit *Commit) Height() int64 { commit.memoizeHeightRound() return commit.height } // Round returns the round of the commit func (commit *Commit) Round() int { commit.memoizeHeightRound() return commit.round } // Type returns the vote type of the commit, which is always VoteTypePrecommit func (commit *Commit) Type() byte { return byte(PrecommitType) } // Size returns the number of votes in the commit func (commit *Commit) Size() int { if commit == nil { return 0 } return len(commit.Precommits) } // BitArray returns a BitArray of which validators voted in this commit func (commit *Commit) BitArray() *cmn.BitArray { if commit.bitArray == nil { commit.bitArray = cmn.NewBitArray(len(commit.Precommits)) for i, precommit := range commit.Precommits { // TODO: need to check the BlockID otherwise we could be counting conflicts, // not just the one with +2/3 ! commit.bitArray.SetIndex(i, precommit != nil) } } return commit.bitArray } // GetByIndex returns the vote corresponding to a given validator index. // Panics if `index >= commit.Size()`. // Implements VoteSetReader. func (commit *Commit) GetByIndex(index int) *Vote { return commit.ToVote(commit.Precommits[index]) } // IsCommit returns true if there is at least one vote. func (commit *Commit) IsCommit() bool { return len(commit.Precommits) != 0 } // ValidateBasic performs basic validation that doesn't involve state data. // Does not actually check the cryptographic signatures. func (commit *Commit) ValidateBasic() error { if commit.BlockID.IsZero() { return errors.New("Commit cannot be for nil block") } if len(commit.Precommits) == 0 { return errors.New("No precommits in commit") } height, round := commit.Height(), commit.Round() // Validate the precommits. for _, precommit := range commit.Precommits { // It's OK for precommits to be missing. if precommit == nil { continue } // Ensure that all votes are precommits. if precommit.Type != PrecommitType { return fmt.Errorf("Invalid commit vote. Expected precommit, got %v", precommit.Type) } // Ensure that all heights are the same. if precommit.Height != height { return fmt.Errorf("Invalid commit precommit height. Expected %v, got %v", height, precommit.Height) } // Ensure that all rounds are the same. if precommit.Round != round { return fmt.Errorf("Invalid commit precommit round. Expected %v, got %v", round, precommit.Round) } } return nil } // Hash returns the hash of the commit func (commit *Commit) Hash() cmn.HexBytes { if commit == nil { return nil } if commit.hash == nil { bs := make([][]byte, len(commit.Precommits)) for i, precommit := range commit.Precommits { bs[i] = cdcEncode(precommit) } commit.hash = merkle.SimpleHashFromByteSlices(bs) } return commit.hash } // StringIndented returns a string representation of the commit func (commit *Commit) StringIndented(indent string) string { if commit == nil { return "nil-Commit" } precommitStrings := make([]string, len(commit.Precommits)) for i, precommit := range commit.Precommits { precommitStrings[i] = precommit.String() } return fmt.Sprintf(`Commit{ %s BlockID: %v %s Precommits: %s %v %s}#%v`, indent, commit.BlockID, indent, indent, strings.Join(precommitStrings, "\n"+indent+" "), indent, commit.hash) } //----------------------------------------------------------------------------- // SignedHeader is a header along with the commits that prove it. // It is the basis of the lite client. type SignedHeader struct { *Header `json:"header"` Commit *Commit `json:"commit"` } // ValidateBasic does basic consistency checks and makes sure the header // and commit are consistent. // // NOTE: This does not actually check the cryptographic signatures. Make // sure to use a Verifier to validate the signatures actually provide a // significantly strong proof for this header's validity. func (sh SignedHeader) ValidateBasic(chainID string) error { // Make sure the header is consistent with the commit. if sh.Header == nil { return errors.New("SignedHeader missing header.") } if sh.Commit == nil { return errors.New("SignedHeader missing commit (precommit votes).") } // Check ChainID. if sh.ChainID != chainID { return fmt.Errorf("Header belongs to another chain '%s' not '%s'", sh.ChainID, chainID) } // Check Height. if sh.Commit.Height() != sh.Height { return fmt.Errorf("SignedHeader header and commit height mismatch: %v vs %v", sh.Height, sh.Commit.Height()) } // Check Hash. hhash := sh.Hash() chash := sh.Commit.BlockID.Hash if !bytes.Equal(hhash, chash) { return fmt.Errorf("SignedHeader commit signs block %X, header is block %X", chash, hhash) } // ValidateBasic on the Commit. err := sh.Commit.ValidateBasic() if err != nil { return cmn.ErrorWrap(err, "commit.ValidateBasic failed during SignedHeader.ValidateBasic") } return nil } func (sh SignedHeader) String() string { return sh.StringIndented("") } // StringIndented returns a string representation of the SignedHeader. func (sh SignedHeader) StringIndented(indent string) string { return fmt.Sprintf(`SignedHeader{ %s %v %s %v %s}`, indent, sh.Header.StringIndented(indent+" "), indent, sh.Commit.StringIndented(indent+" "), indent) } //----------------------------------------------------------------------------- // Data contains the set of transactions included in the block type Data struct { // Txs that will be applied by state @ block.Height+1. // NOTE: not all txs here are valid. We're just agreeing on the order first. // This means that block.AppHash does not include these txs. Txs Txs `json:"txs"` // Volatile hash cmn.HexBytes } // Hash returns the hash of the data func (data *Data) Hash() cmn.HexBytes { if data == nil { return (Txs{}).Hash() } if data.hash == nil { data.hash = data.Txs.Hash() // NOTE: leaves of merkle tree are TxIDs } return data.hash } // StringIndented returns a string representation of the transactions func (data *Data) StringIndented(indent string) string { if data == nil { return "nil-Data" } txStrings := make([]string, cmn.MinInt(len(data.Txs), 21)) for i, tx := range data.Txs { if i == 20 { txStrings[i] = fmt.Sprintf("... (%v total)", len(data.Txs)) break } txStrings[i] = fmt.Sprintf("%X (%d bytes)", tx.Hash(), len(tx)) } return fmt.Sprintf(`Data{ %s %v %s}#%v`, indent, strings.Join(txStrings, "\n"+indent+" "), indent, data.hash) } //----------------------------------------------------------------------------- // EvidenceData contains any evidence of malicious wrong-doing by validators type EvidenceData struct { Evidence EvidenceList `json:"evidence"` // Volatile hash cmn.HexBytes } // Hash returns the hash of the data. func (data *EvidenceData) Hash() cmn.HexBytes { if data.hash == nil { data.hash = data.Evidence.Hash() } return data.hash } // StringIndented returns a string representation of the evidence. func (data *EvidenceData) StringIndented(indent string) string { if data == nil { return "nil-Evidence" } evStrings := make([]string, cmn.MinInt(len(data.Evidence), 21)) for i, ev := range data.Evidence { if i == 20 { evStrings[i] = fmt.Sprintf("... (%v total)", len(data.Evidence)) break } evStrings[i] = fmt.Sprintf("Evidence:%v", ev) } return fmt.Sprintf(`EvidenceData{ %s %v %s}#%v`, indent, strings.Join(evStrings, "\n"+indent+" "), indent, data.hash) } //-------------------------------------------------------------------------------- // BlockID defines the unique ID of a block as its Hash and its PartSetHeader type BlockID struct { Hash cmn.HexBytes `json:"hash"` PartsHeader PartSetHeader `json:"parts"` } // Equals returns true if the BlockID matches the given BlockID func (blockID BlockID) Equals(other BlockID) bool { return bytes.Equal(blockID.Hash, other.Hash) && blockID.PartsHeader.Equals(other.PartsHeader) } // Key returns a machine-readable string representation of the BlockID func (blockID BlockID) Key() string { bz, err := cdc.MarshalBinaryBare(blockID.PartsHeader) if err != nil { panic(err) } return string(blockID.Hash) + string(bz) } // ValidateBasic performs basic validation. func (blockID BlockID) ValidateBasic() error { // Hash can be empty in case of POLBlockID in Proposal. if err := ValidateHash(blockID.Hash); err != nil { return fmt.Errorf("Wrong Hash") } if err := blockID.PartsHeader.ValidateBasic(); err != nil { return fmt.Errorf("Wrong PartsHeader: %v", err) } return nil } // IsZero returns true if this is the BlockID of a nil block. func (blockID BlockID) IsZero() bool { return len(blockID.Hash) == 0 && blockID.PartsHeader.IsZero() } // IsComplete returns true if this is a valid BlockID of a non-nil block. func (blockID BlockID) IsComplete() bool { return len(blockID.Hash) == tmhash.Size && blockID.PartsHeader.Total > 0 && len(blockID.PartsHeader.Hash) == tmhash.Size } // String returns a human readable string representation of the BlockID func (blockID BlockID) String() string { return fmt.Sprintf(`%v:%v`, blockID.Hash, blockID.PartsHeader) }