Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Justified revision #802

Merged
merged 16 commits into from
Sep 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions api/accounts/accounts.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,15 @@ type Accounts struct {
stater *state.Stater
callGasLimit uint64
forkConfig thor.ForkConfig
bft bft.Finalizer
bft bft.Committer
}

func New(
repo *chain.Repository,
stater *state.Stater,
callGasLimit uint64,
forkConfig thor.ForkConfig,
bft bft.Finalizer,
paologalligit marked this conversation as resolved.
Show resolved Hide resolved
bft bft.Committer,
) *Accounts {
return &Accounts{
repo,
Expand Down
2 changes: 1 addition & 1 deletion api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ func New(
stater *state.Stater,
txPool *txpool.TxPool,
logDB *logdb.LogDB,
bft bft.Finalizer,
bft bft.Committer,
nw node.Network,
forkConfig thor.ForkConfig,
allowedOrigins string,
Expand Down
4 changes: 2 additions & 2 deletions api/blocks/blocks.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@ import (

type Blocks struct {
repo *chain.Repository
bft bft.Finalizer
bft bft.Committer
}

func New(repo *chain.Repository, bft bft.Finalizer) *Blocks {
func New(repo *chain.Repository, bft bft.Committer) *Blocks {
return &Blocks{
repo,
bft,
Expand Down
12 changes: 12 additions & 0 deletions api/blocks/blocks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"github.com/ethereum/go-ethereum/crypto"
"github.com/gorilla/mux"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/vechain/thor/v2/api/blocks"
"github.com/vechain/thor/v2/block"
"github.com/vechain/thor/v2/chain"
Expand Down Expand Up @@ -52,6 +53,7 @@ func TestBlock(t *testing.T) {
"testGetBlockByHeight": testGetBlockByHeight,
"testGetBestBlock": testGetBestBlock,
"testGetFinalizedBlock": testGetFinalizedBlock,
"testGetJustifiedBlock": testGetJustifiedBlock,
"testGetBlockWithRevisionNumberTooHigh": testGetBlockWithRevisionNumberTooHigh,
} {
t.Run(name, tt)
Expand Down Expand Up @@ -99,6 +101,16 @@ func testGetFinalizedBlock(t *testing.T) {
assert.Equal(t, genesisBlock.Header().ID(), finalized.ID)
}

func testGetJustifiedBlock(t *testing.T) {
res, statusCode := httpGet(t, ts.URL+"/blocks/justified")
justified := new(blocks.JSONCollapsedBlock)
require.NoError(t, json.Unmarshal(res, &justified))

assert.Equal(t, http.StatusOK, statusCode)
assert.Equal(t, uint32(0), justified.Number)
assert.Equal(t, genesisBlock.Header().ID(), justified.ID)
otherview marked this conversation as resolved.
Show resolved Hide resolved
}

func testGetBlockById(t *testing.T) {
res, statusCode := httpGet(t, ts.URL+"/blocks/"+blk.Header().ID().String())
rb := new(blocks.JSONCollapsedBlock)
Expand Down
6 changes: 3 additions & 3 deletions api/debug/debug.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ type Debug struct {
forkConfig thor.ForkConfig
callGasLimit uint64
allowCustomTracer bool
bft bft.Finalizer
allowedTracers map[string]interface{}
bft bft.Committer
skipPoA bool
}

Expand All @@ -54,7 +54,7 @@ func New(
forkConfig thor.ForkConfig,
callGaslimit uint64,
allowCustomTracer bool,
bft bft.Finalizer,
bft bft.Committer,
allowedTracers map[string]interface{},
soloMode bool) *Debug {
return &Debug{
Expand All @@ -63,8 +63,8 @@ func New(
forkConfig,
callGaslimit,
allowCustomTracer,
bft,
allowedTracers,
bft,
soloMode,
}
}
Expand Down
5 changes: 3 additions & 2 deletions api/doc/thor.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2252,15 +2252,15 @@ components:
RevisionInQuery:
name: revision
in: query
description: Specify either `best`, `finalized`, a block number or block ID. If omitted, the `best` block is assumed.
description: Specify either `best`, `justified`, `finalized`, a block number or block ID. If omitted, the `best` block is assumed.
schema:
type: string

CallCodeRevisionInQuery:
name: revision
in: query
description: |
Specify either `best`,`next`, `finalized`, a block number or block ID. If omitted, the `best` block is assumed.
Specify either `best`, `next`, `justified`, `finalized`, a block number or block ID. If omitted, the `best` block is assumed.

If the `next` block is specified, the call code will be executed on the next block, with the following:
- The block number is the `best` block number plus one.
Expand All @@ -2279,6 +2279,7 @@ components:
- a block ID (hex string)
- a block number (integer)
- `best` stands for latest block
- `justified` stands for the justified block
- `finalized` stands for the finalized block
required: true
schema:
Expand Down
14 changes: 12 additions & 2 deletions api/utils/revisions.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const (
revBest int64 = -1
revFinalized int64 = -2
revNext int64 = -3
revJustified int64 = -4
)

type Revision struct {
Expand All @@ -41,6 +42,10 @@ func ParseRevision(revision string, allowNext bool) (*Revision, error) {
return &Revision{revFinalized}, nil
}

if revision == "justified" {
return &Revision{revJustified}, nil
}

otherview marked this conversation as resolved.
Show resolved Hide resolved
if revision == "next" {
if !allowNext {
return nil, errors.New("invalid revision: next is not allowed")
Expand All @@ -67,7 +72,7 @@ func ParseRevision(revision string, allowNext bool) (*Revision, error) {

// GetSummary returns the block summary for the given revision,
// revision required to be a deterministic block other than "next".
func GetSummary(rev *Revision, repo *chain.Repository, bft bft.Finalizer) (sum *chain.BlockSummary, err error) {
func GetSummary(rev *Revision, repo *chain.Repository, bft bft.Committer) (sum *chain.BlockSummary, err error) {
var id thor.Bytes32
switch rev := rev.val.(type) {
case thor.Bytes32:
Expand All @@ -83,6 +88,11 @@ func GetSummary(rev *Revision, repo *chain.Repository, bft bft.Finalizer) (sum *
id = repo.BestBlockSummary().Header.ID()
case revFinalized:
id = bft.Finalized()
case revJustified:
id, err = bft.Justified()
if err != nil {
return nil, err
}
}
}
if id.IsZero() {
Expand All @@ -97,7 +107,7 @@ func GetSummary(rev *Revision, repo *chain.Repository, bft bft.Finalizer) (sum *

// GetSummaryAndState returns the block summary and state for the given revision,
// this function supports the "next" revision.
func GetSummaryAndState(rev *Revision, repo *chain.Repository, bft bft.Finalizer, stater *state.Stater) (*chain.BlockSummary, *state.State, error) {
func GetSummaryAndState(rev *Revision, repo *chain.Repository, bft bft.Committer, stater *state.Stater) (*chain.BlockSummary, *state.State, error) {
if rev.IsNext() {
best := repo.BestBlockSummary()

Expand Down
10 changes: 10 additions & 0 deletions api/utils/revisions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ func TestParseRevision(t *testing.T) {
err: nil,
expected: &Revision{revBest},
},
{
revision: "justified",
err: nil,
expected: &Revision{revJustified},
},
{
revision: "finalized",
err: nil,
Expand Down Expand Up @@ -122,6 +127,11 @@ func TestGetSummary(t *testing.T) {
revision: &Revision{uint32(1234)},
err: errors.New("not found"),
},
{
name: "justified",
revision: &Revision{revJustified},
err: nil,
},
{
name: "finalized",
revision: &Revision{revFinalized},
Expand Down
81 changes: 73 additions & 8 deletions bft/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,14 @@ const dataStoreName = "bft.engine"

var finalizedKey = []byte("finalized")

type Finalizer interface {
type Committer interface {
Finalized() thor.Bytes32
Justified() (thor.Bytes32, error)
}

type justified struct {
search thor.Bytes32
value thor.Bytes32
}

// BFTEngine tracks all votes of blocks, computes the finalized checkpoint.
Expand All @@ -39,6 +45,7 @@ type BFTEngine struct {
master thor.Address
casts casts
finalized atomic.Value
justified atomic.Value
caches struct {
state *lru.Cache
quality *lru.Cache
Expand All @@ -60,6 +67,7 @@ func NewEngine(repo *chain.Repository, mainDB *muxdb.MuxDB, forkConfig thor.Fork
engine.caches.quality, _ = lru.New(16)
engine.caches.justifier = cache.NewPrioCache(16)

// Restore finalized block, if any
if val, err := engine.data.Get(finalizedKey); err != nil {
if !engine.data.IsNotFound(err) {
return nil, err
Expand All @@ -77,6 +85,54 @@ func (engine *BFTEngine) Finalized() thor.Bytes32 {
return engine.finalized.Load().(thor.Bytes32)
}

// Justified returns the justified checkpoint.
func (engine *BFTEngine) Justified() (thor.Bytes32, error) {
head := engine.repo.BestBlockSummary().Header
finalized := engine.Finalized()

// if head is in the first epoch and not concluded yet
if head.Number() < getCheckPoint(engine.forkConfig.FINALITY)+thor.CheckpointInterval-1 {
return finalized, nil
}

// find the recent concluded checkpoint
concluded := getCheckPoint(head.Number())
if head.Number() < getStorePoint(head.Number()) {
concluded -= thor.CheckpointInterval
}

headChain := engine.repo.NewChain(head.ID())

// storeID is the block id where an epoch concluded
storeID, err := headChain.GetBlockID(getStorePoint(concluded))
if err != nil {
return thor.Bytes32{}, err
}

if val := engine.justified.Load(); val != nil && storeID == val.(justified).search {
return val.(justified).value, nil
}

quality, err := engine.getQuality(storeID)
if err != nil {
return thor.Bytes32{}, err
}

// if the quality is 0, then the epoch is not justified
// this is possible for the starting epochs
if quality == 0 {
return finalized, nil
}

checkpoint, err := engine.findCheckpointByQuality(quality, finalized, storeID)
if err != nil {
return thor.Bytes32{}, err
}

engine.justified.Store(justified{search: storeID, value: checkpoint})
return checkpoint, nil
}

// Accepts checks if the given block is on the same branch of finalized checkpoint.
func (engine *BFTEngine) Accepts(parentID thor.Bytes32) (bool, error) {
finalized := engine.Finalized()
Expand Down Expand Up @@ -123,7 +179,7 @@ func (engine *BFTEngine) CommitBlock(header *block.Header, isPacking bool) error
engine.caches.quality.Add(header.ID(), state.Quality)

if state.Committed && state.Quality > 1 {
id, err := engine.findCheckpointByQuality(state.Quality-1, engine.Finalized(), header.ParentID())
id, err := engine.findCheckpointByQuality(state.Quality-1, engine.Finalized(), header.ID())
if err != nil {
return err
}
Expand Down Expand Up @@ -182,17 +238,23 @@ func (engine *BFTEngine) ShouldVote(parentID thor.Bytes32) (bool, error) {

headQuality := st.Quality
finalized := engine.Finalized()
chain := engine.repo.NewChain(parentID)
// most recent justified checkpoint
var recentJC thor.Bytes32
if st.Justified {
// if justified in this round, use this round's checkpoint
checkpoint, err := engine.repo.NewChain(parentID).GetBlockID(getCheckPoint(block.Number(parentID)))
checkpoint, err := chain.GetBlockID(getCheckPoint(block.Number(parentID)))
if err != nil {
return false, err
}
recentJC = checkpoint
} else {
checkpoint, err := engine.findCheckpointByQuality(headQuality, finalized, parentID)
// if current round is not justified, find the most recent justified checkpoint
prev, err := chain.GetBlockID(getStorePoint(block.Number(parentID) - thor.CheckpointInterval))
if err != nil {
return false, err
}
checkpoint, err := engine.findCheckpointByQuality(headQuality, finalized, prev)
if err != nil {
return false, err
}
Expand Down Expand Up @@ -223,7 +285,7 @@ func (engine *BFTEngine) ShouldVote(parentID thor.Bytes32) (bool, error) {
return true, nil
}

// computeState computes the bft state regarding the given block header.
// computeState computes the bft state regarding the given block header to the closest checkpoint.
func (engine *BFTEngine) computeState(header *block.Header) (*bftState, error) {
if cached, ok := engine.caches.state.Get(header.ID()); ok {
return cached.(*bftState), nil
Expand Down Expand Up @@ -278,7 +340,8 @@ func (engine *BFTEngine) computeState(header *block.Header) (*bftState, error) {
}

// findCheckpointByQuality finds the first checkpoint reaches the given quality.
func (engine *BFTEngine) findCheckpointByQuality(target uint32, finalized, parentID thor.Bytes32) (blockID thor.Bytes32, err error) {
// It is caller's responsibility to ensure the epoch that headID belongs to is concluded.
func (engine *BFTEngine) findCheckpointByQuality(target uint32, finalized, headID thor.Bytes32) (blockID thor.Bytes32, err error) {
defer func() {
if e := recover(); e != nil {
err = e.(error)
Expand All @@ -291,7 +354,7 @@ func (engine *BFTEngine) findCheckpointByQuality(target uint32, finalized, paren
searchStart = getCheckPoint(engine.forkConfig.FINALITY)
}

c := engine.repo.NewChain(parentID)
c := engine.repo.NewChain(headID)
get := func(i int) (uint32, error) {
id, err := c.GetBlockID(getStorePoint(searchStart + uint32(i)*thor.CheckpointInterval))
if err != nil {
Expand All @@ -300,7 +363,8 @@ func (engine *BFTEngine) findCheckpointByQuality(target uint32, finalized, paren
return engine.getQuality(id)
}

n := int((block.Number(parentID) + 1 - searchStart) / thor.CheckpointInterval)
// sort.Search searches from [0, n)
n := int((block.Number(headID)-searchStart)/thor.CheckpointInterval) + 1
num := sort.Search(n, func(i int) bool {
quality, err := get(i)
if err != nil {
Expand All @@ -310,6 +374,7 @@ func (engine *BFTEngine) findCheckpointByQuality(target uint32, finalized, paren
return quality >= target
})

// n means not found for sort.Search
if num == n {
return thor.Bytes32{}, errors.New("failed find the block by quality")
}
Expand Down
Loading
Loading