lotus/chain/types/tipset.go
2023-03-10 13:21:17 -08:00

269 lines
5.4 KiB
Go

package types
import (
"bytes"
"encoding/json"
"fmt"
"io"
"sort"
"github.com/ipfs/go-cid"
logging "github.com/ipfs/go-log/v2"
"github.com/minio/blake2b-simd"
cbg "github.com/whyrusleeping/cbor-gen"
"golang.org/x/xerrors"
"github.com/filecoin-project/go-state-types/abi"
)
var log = logging.Logger("types")
type TipSet struct {
cids []cid.Cid
blks []*BlockHeader
height abi.ChainEpoch
}
type ExpTipSet struct {
Cids []cid.Cid
Blocks []*BlockHeader
Height abi.ChainEpoch
}
func (ts *TipSet) MarshalJSON() ([]byte, error) {
// why didnt i just export the fields? Because the struct has methods with the
// same names already
return json.Marshal(ExpTipSet{
Cids: ts.cids,
Blocks: ts.blks,
Height: ts.height,
})
}
func (ts *TipSet) UnmarshalJSON(b []byte) error {
var ets ExpTipSet
if err := json.Unmarshal(b, &ets); err != nil {
return err
}
ots, err := NewTipSet(ets.Blocks)
if err != nil {
return err
}
*ts = *ots
return nil
}
func (ts *TipSet) MarshalCBOR(w io.Writer) error {
if ts == nil {
_, err := w.Write(cbg.CborNull)
return err
}
return (&ExpTipSet{
Cids: ts.cids,
Blocks: ts.blks,
Height: ts.height,
}).MarshalCBOR(w)
}
func (ts *TipSet) UnmarshalCBOR(r io.Reader) error {
var ets ExpTipSet
if err := ets.UnmarshalCBOR(r); err != nil {
return err
}
ots, err := NewTipSet(ets.Blocks)
if err != nil {
return err
}
*ts = *ots
return nil
}
func tipsetSortFunc(blks []*BlockHeader) func(i, j int) bool {
return func(i, j int) bool {
ti := blks[i].LastTicket()
tj := blks[j].LastTicket()
if ti.Equals(tj) {
log.Warnf("blocks have same ticket (%s %s)", blks[i].Miner, blks[j].Miner)
return bytes.Compare(blks[i].Cid().Bytes(), blks[j].Cid().Bytes()) < 0
}
return ti.Less(tj)
}
}
// Checks:
// - A tipset is composed of at least one block. (Because of our variable
// number of blocks per tipset, determined by randomness, we do not impose
// an upper limit.)
// - All blocks have the same height.
// - All blocks have the same parents (same number of them and matching CIDs).
func NewTipSet(blks []*BlockHeader) (*TipSet, error) {
if len(blks) == 0 {
return nil, xerrors.Errorf("NewTipSet called with zero length array of blocks")
}
sort.Slice(blks, tipsetSortFunc(blks))
var ts TipSet
ts.cids = []cid.Cid{blks[0].Cid()}
ts.blks = blks
for _, b := range blks[1:] {
if b.Height != blks[0].Height {
return nil, fmt.Errorf("cannot create tipset with mismatching heights")
}
if len(blks[0].Parents) != len(b.Parents) {
return nil, fmt.Errorf("cannot create tipset with mismatching number of parents")
}
for i, cid := range b.Parents {
if cid != blks[0].Parents[i] {
return nil, fmt.Errorf("cannot create tipset with mismatching parents")
}
}
ts.cids = append(ts.cids, b.Cid())
}
ts.height = blks[0].Height
return &ts, nil
}
func (ts *TipSet) Cids() []cid.Cid {
return ts.cids
}
func (ts *TipSet) Key() TipSetKey {
if ts == nil {
return EmptyTSK
}
return NewTipSetKey(ts.cids...)
}
func (ts *TipSet) Height() abi.ChainEpoch {
return ts.height
}
func (ts *TipSet) Parents() TipSetKey {
return NewTipSetKey(ts.blks[0].Parents...)
}
func (ts *TipSet) Blocks() []*BlockHeader {
return ts.blks
}
func (ts *TipSet) Equals(ots *TipSet) bool {
if ts == nil && ots == nil {
return true
}
if ts == nil || ots == nil {
return false
}
if ts.height != ots.height {
return false
}
if len(ts.cids) != len(ots.cids) {
return false
}
for i, cid := range ts.cids {
if cid != ots.cids[i] {
return false
}
}
return true
}
func (t *Ticket) Less(o *Ticket) bool {
tDigest := blake2b.Sum256(t.VRFProof)
oDigest := blake2b.Sum256(o.VRFProof)
return bytes.Compare(tDigest[:], oDigest[:]) < 0
}
func (ts *TipSet) MinTicket() *Ticket {
return ts.MinTicketBlock().Ticket
}
func (ts *TipSet) MinTimestamp() uint64 {
if ts == nil {
return 0
}
blks := ts.Blocks()
// TODO::FVM @vyzo @magik Null rounds shouldn't ever be represented as
// tipsets with no blocks; Null-round generally means that the tipset at
// that epoch doesn't exist - and the next tipset that does exist links
// straight to first epoch with blocks (@raulk agrees -- this is odd)
if len(blks) == 0 {
// null rounds make things crash -- it is threaded in every fvm instantiation
return 0
}
minTs := blks[0].Timestamp
for _, bh := range blks[1:] {
if bh.Timestamp < minTs {
minTs = bh.Timestamp
}
}
return minTs
}
func (ts *TipSet) MinTicketBlock() *BlockHeader {
blks := ts.Blocks()
min := blks[0]
for _, b := range blks[1:] {
if b.LastTicket().Less(min.LastTicket()) {
min = b
}
}
return min
}
func (ts *TipSet) ParentMessageReceipts() cid.Cid {
return ts.blks[0].ParentMessageReceipts
}
func (ts *TipSet) ParentState() cid.Cid {
return ts.blks[0].ParentStateRoot
}
func (ts *TipSet) ParentWeight() BigInt {
return ts.blks[0].ParentWeight
}
func (ts *TipSet) Contains(oc cid.Cid) bool {
for _, c := range ts.cids {
if c == oc {
return true
}
}
return false
}
func (ts *TipSet) IsChildOf(parent *TipSet) bool {
return CidArrsEqual(ts.Parents().Cids(), parent.Cids()) &&
// FIXME: The height check might go beyond what is meant by
// "parent", but many parts of the code rely on the tipset's
// height for their processing logic at the moment to obviate it.
ts.height > parent.height
}
func (ts *TipSet) String() string {
return fmt.Sprintf("%v", ts.cids)
}