Skip to content

Commit c73083a

Browse files
Merge branch 'master' into darren/refactor/upgrade-cli-to-v3
2 parents 56ad0d3 + aba12df commit c73083a

File tree

13 files changed

+224
-42
lines changed

13 files changed

+224
-42
lines changed

.github/workflows/on-release.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ jobs:
3636
- name: Validate API Version
3737
run: |
3838
thor_version=$(cat cmd/thor/VERSION)
39-
api_version=$(grep -o 'version: [0-9.]*' api/doc/thor.yaml | awk '{print $2}')
39+
api_version=$(grep -o 'version: [0-9][0-9.]*' api/doc/thor.yaml | awk '{print $2}')
4040
if [ "$thor_version" != "$api_version" ]; then
4141
echo "API spec version ($api_version) does not match Thor version ($thor_version)"
4242
exit 1

chain/chain.go

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,6 @@ import (
2424
"github.com/vechain/thor/v2/tx"
2525
)
2626

27-
const (
28-
// IndexTrieName is the name of index trie.
29-
// The index tire is used to store mappings from block number to block id, and tx id to tx meta.
30-
IndexTrieName = "i"
31-
)
32-
3327
type storageTxMeta struct {
3428
Index uint64
3529
Reverted bool
@@ -57,6 +51,7 @@ type Chain struct {
5751

5852
func newChain(repo *Repository, headID thor.Bytes32) *Chain {
5953
var (
54+
// The index tire is used to store mappings from block number to block id, and tx id to tx meta.
6055
indexTrie *muxdb.Trie
6156
initErr error
6257
)
@@ -67,7 +62,7 @@ func newChain(repo *Repository, headID thor.Bytes32) *Chain {
6762
func() (*muxdb.Trie, error) {
6863
if indexTrie == nil && initErr == nil {
6964
if summary, err := repo.GetBlockSummary(headID); err == nil {
70-
indexTrie = repo.db.NewTrie(IndexTrieName, summary.IndexRoot())
65+
indexTrie = repo.db.NewTrie(muxdb.IndexTrieName, summary.IndexRoot())
7166
} else {
7267
initErr = errors.Wrap(err, fmt.Sprintf("lazy init chain, head=%v", headID))
7368
}
@@ -373,7 +368,7 @@ func (r *Repository) NewChain(headID thor.Bytes32) *Chain {
373368
}
374369

375370
func (r *Repository) indexBlock(parentRoot trie.Root, newBlockID thor.Bytes32, newConflicts uint32) error {
376-
t := r.db.NewTrie(IndexTrieName, parentRoot)
371+
t := r.db.NewTrie(muxdb.IndexTrieName, parentRoot)
377372
// map block number to block ID
378373
if err := t.Update(newBlockID[:4], newBlockID[:], nil); err != nil {
379374
return err

cmd/thor/main.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,7 @@ func defaultAction(_ context.Context, ctx *cli.Command) error {
320320
defer p2pCommunicator.Stop()
321321

322322
if !ctx.Bool(disablePrunerFlag.Name) {
323-
pruner := pruner.New(mainDB, repo, bftEngine)
323+
pruner := pruner.New(mainDB, repo, bftEngine, *forkConfig)
324324
defer func() { log.Info("stopping pruner..."); pruner.Stop() }()
325325
}
326326

@@ -510,7 +510,7 @@ func soloAction(_ context.Context, ctx *cli.Command) error {
510510
printStartupMessage2(gene, apiURL, "", metricsURL, adminURL, isDevnet)
511511

512512
if !ctx.Bool(disablePrunerFlag.Name) {
513-
pruner := pruner.New(mainDB, repo, bftMockedEngine)
513+
pruner := pruner.New(mainDB, repo, bftMockedEngine, *forkConfig)
514514
defer func() { log.Info("stopping pruner..."); pruner.Stop() }()
515515
}
516516

cmd/thor/pruner/pruner.go

Lines changed: 63 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ package pruner
88
import (
99
"context"
1010
"fmt"
11+
"math"
1112
"sync"
1213
"time"
1314

@@ -40,17 +41,19 @@ type Pruner struct {
4041
commiter bft.Committer
4142
cancel func()
4243
goes sync.WaitGroup
44+
fc *thor.ForkConfig
4345
}
4446

4547
// New creates and starts the pruner.
46-
func New(db *muxdb.MuxDB, repo *chain.Repository, commiter bft.Committer) *Pruner {
48+
func New(db *muxdb.MuxDB, repo *chain.Repository, commiter bft.Committer, fc thor.ForkConfig) *Pruner {
4749
ctx, cancel := context.WithCancel(context.Background())
4850
o := &Pruner{
4951
db: db,
5052
repo: repo,
5153
ctx: ctx,
5254
commiter: commiter,
5355
cancel: cancel,
56+
fc: &fc,
5457
}
5558
o.goes.Go(func() {
5659
if err := o.loop(); err != nil {
@@ -90,9 +93,11 @@ func (p *Pruner) loop() error {
9093
// select target
9194
target := status.Base + period
9295

93-
targetChain, err := p.awaitUntilFinalized(target + thor.MaxStateHistory)
96+
// adding thor.MaxStateHistory here since we need to ensure that defined range of history is required
97+
// to be kept for EVM accessibility. It's defined in thor/params.go(thor.MaxStateHistory ~7 days).
98+
targetChain, err := p.awaitUntilPrunable(target + thor.MaxStateHistory)
9499
if err != nil {
95-
return errors.Wrap(err, "awaitUntilFinalized")
100+
return errors.Wrap(err, "awaitUntilPrunable")
96101
}
97102
startTime := time.Now().UnixNano()
98103

@@ -153,15 +158,15 @@ func (p *Pruner) checkpointTries(targetChain *chain.Chain, base, target uint32)
153158
}
154159

155160
// checkpoint index trie
156-
indexTrie := p.db.NewTrie(chain.IndexTrieName, summary.IndexRoot())
161+
indexTrie := p.db.NewTrie(muxdb.IndexTrieName, summary.IndexRoot())
157162
indexTrie.SetNoFillCache(true)
158163

159164
if err := indexTrie.Checkpoint(p.ctx, base, nil); err != nil {
160165
return err
161166
}
162167

163168
// checkpoint account trie
164-
accTrie := p.db.NewTrie(state.AccountTrieName, summary.Root())
169+
accTrie := p.db.NewTrie(muxdb.AccountTrieName, summary.Root())
165170
accTrie.SetNoFillCache(true)
166171

167172
var sTries []*muxdb.Trie
@@ -195,9 +200,13 @@ func (p *Pruner) pruneTries(targetChain *chain.Chain, base, target uint32) error
195200
return nil
196201
}
197202

198-
// awaitUntilFinalized waits until the target block number becomes almost final(steady),
199-
// and returns the steady chain.
200-
func (p *Pruner) awaitUntilFinalized(target uint32) (*chain.Chain, error) {
203+
// awaitUntilPrunable waits until the target block number becomes prunable,and returns the prunable chain.
204+
// Before the finality hard fork, it's awaitUntilSteady. After the finality hard fork, it's awaitUntilFinalized.
205+
func (p *Pruner) awaitUntilPrunable(target uint32) (*chain.Chain, error) {
206+
if p.fc.FINALITY > target {
207+
return p.awaitUntilSteady(target)
208+
}
209+
201210
for {
202211
finalizedID := p.commiter.Finalized()
203212
finalizedNum := block.Number(finalizedID)
@@ -217,3 +226,49 @@ func (p *Pruner) awaitUntilFinalized(target uint32) (*chain.Chain, error) {
217226
}
218227
}
219228
}
229+
230+
func (p *Pruner) awaitUntilSteady(target uint32) (*chain.Chain, error) {
231+
const windowSize = 100000
232+
233+
backoff := uint32(0)
234+
for {
235+
best := p.repo.BestBlockSummary()
236+
bestNum := best.Header.Number()
237+
if bestNum > target+backoff {
238+
var meanScore float64
239+
if bestNum > windowSize {
240+
baseNum := bestNum - windowSize
241+
baseHeader, err := p.repo.NewChain(best.Header.ID()).GetBlockHeader(baseNum)
242+
if err != nil {
243+
return nil, err
244+
}
245+
meanScore = math.Round(float64(best.Header.TotalScore()-baseHeader.TotalScore()) / float64(windowSize))
246+
} else {
247+
meanScore = math.Round(float64(best.Header.TotalScore()) / float64(bestNum))
248+
}
249+
set := make(map[thor.Address]struct{})
250+
// reverse iterate the chain and collect signers.
251+
for i, prev := 0, best.Header; i < int(meanScore*3) && prev.Number() >= target; i++ {
252+
signer, _ := prev.Signer()
253+
set[signer] = struct{}{}
254+
if len(set) >= int(math.Round((meanScore+1)/2)) {
255+
// got enough unique signers
256+
steadyID := prev.ID()
257+
return p.repo.NewChain(steadyID), nil
258+
}
259+
parent, err := p.repo.GetBlockSummary(prev.ParentID())
260+
if err != nil {
261+
return nil, err
262+
}
263+
prev = parent.Header
264+
}
265+
backoff += uint32(meanScore)
266+
} else {
267+
select {
268+
case <-p.ctx.Done():
269+
return nil, p.ctx.Err()
270+
case <-time.After(time.Second):
271+
}
272+
}
273+
}
274+
}

cmd/thor/pruner/pruner_test.go

Lines changed: 112 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@ import (
1313
"math/big"
1414
"os"
1515
"path/filepath"
16+
"slices"
1617
"testing"
1718
"time"
1819

1920
"github.com/vechain/thor/v2/bft"
21+
"github.com/vechain/thor/v2/builtin"
2022
"github.com/vechain/thor/v2/test/testchain"
2123

2224
"github.com/ethereum/go-ethereum/crypto"
@@ -108,12 +110,12 @@ func TestStatus(t *testing.T) {
108110
func TestNewPruner(t *testing.T) {
109111
db := muxdb.NewMem()
110112
stater := state.NewStater(db)
111-
gene, _ := genesis.NewDevnet()
113+
gene, fc := genesis.NewDevnet()
112114
b0, _, _, _ := gene.Build(stater)
113115
repo, _ := chain.NewRepository(db, b0)
114116

115117
bftMockedEngine := bft.NewMockedEngine(repo.GenesisBlock().Header().ID())
116-
pr := New(db, repo, bftMockedEngine)
118+
pr := New(db, repo, bftMockedEngine, *fc)
117119
pr.Stop()
118120
}
119121

@@ -188,7 +190,7 @@ func (tc *testCommitter) ShouldVote(parentID thor.Bytes32) (bool, error) {
188190
func TestWaitUntil(t *testing.T) {
189191
db := muxdb.NewMem()
190192
stater := state.NewStater(db)
191-
gene, _ := genesis.NewDevnet()
193+
gene, fc := genesis.NewDevnet()
192194
b0, _, _, _ := gene.Build(stater)
193195
repo, _ := chain.NewRepository(db, b0)
194196
devAccounts := genesis.DevAccounts()
@@ -201,6 +203,7 @@ func TestWaitUntil(t *testing.T) {
201203
ctx: ctx,
202204
commiter: testCommiter,
203205
cancel: cancel,
206+
fc: fc,
204207
}
205208

206209
parentID := b0.Header().ID()
@@ -231,7 +234,7 @@ func TestWaitUntil(t *testing.T) {
231234

232235
cancel()
233236
// Use a target that doesn't exist yet to force waiting (where cancellation is checked)
234-
_, err = pruner.awaitUntilFinalized(200000) // Target beyond current best
237+
_, err = pruner.awaitUntilPrunable(200000) // Target beyond current best
235238
assert.NotNil(t, err)
236239
assert.Equal(t, context.Canceled, err)
237240

@@ -250,7 +253,7 @@ func TestWaitUntil(t *testing.T) {
250253
pruner.ctx = ctx
251254
pruner.cancel = cancel
252255

253-
chain, err := pruner.awaitUntilFinalized(100000)
256+
chain, err := pruner.awaitUntilPrunable(100000)
254257
assert.Nil(t, err)
255258

256259
assert.True(t, block.Number(chain.HeadID()) >= 10000)
@@ -344,3 +347,107 @@ func TestGetAfterPrune(t *testing.T) {
344347
assert.Error(t, err)
345348
assert.Contains(t, err.Error(), "missing trie node")
346349
}
350+
351+
func TestGetStorageRandomlyTouchedAfterPrune(t *testing.T) {
352+
type testcase struct {
353+
name string
354+
blocks []int
355+
expectedEnergy uint64
356+
}
357+
358+
cases := []testcase{
359+
{
360+
name: "touch storage every block",
361+
blocks: []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
362+
expectedEnergy: uint64(10),
363+
},
364+
{
365+
name: "touch only once at 9",
366+
blocks: []int{9},
367+
expectedEnergy: uint64(1),
368+
},
369+
{
370+
name: "touch only once at 10",
371+
blocks: []int{10},
372+
expectedEnergy: uint64(1),
373+
},
374+
{
375+
name: "touch randomly before pruning point case 1",
376+
blocks: []int{5, 7, 8},
377+
expectedEnergy: uint64(3),
378+
},
379+
{
380+
name: "touch randomly before pruning point case 2",
381+
blocks: []int{5, 7},
382+
expectedEnergy: uint64(2),
383+
},
384+
{
385+
name: "touch randomly before pruning point case 3",
386+
blocks: []int{3, 6},
387+
expectedEnergy: uint64(2),
388+
},
389+
}
390+
391+
for _, tc := range cases {
392+
t.Run(tc.name, func(t *testing.T) {
393+
chain, err := testchain.NewDefault()
394+
assert.NoError(t, err)
395+
396+
accounts := genesis.DevAccounts()
397+
to := thor.BytesToAddress([]byte("to"))
398+
399+
// prepare energy transfer data
400+
transferMethod, ok := builtin.Energy.ABI.MethodByName("transfer")
401+
assert.True(t, ok)
402+
transferData, err := transferMethod.EncodeInput(to, big.NewInt(1))
403+
assert.NoError(t, err)
404+
405+
for i := range 20 {
406+
clauses := []*tx.Clause{tx.NewClause(&to).WithValue(big.NewInt(1))}
407+
if contains(tc.blocks, i+1) {
408+
// touch energy storage by transferring 1 wei VTHO
409+
clauses = append(clauses, tx.NewClause(&builtin.Energy.Address).WithData(transferData))
410+
}
411+
412+
err = chain.MintClauses(accounts[0], clauses)
413+
assert.NoError(t, err)
414+
}
415+
416+
pruner := Pruner{
417+
repo: chain.Repo(),
418+
db: chain.Database(),
419+
}
420+
// iterate best chain to 20
421+
422+
// prune [0, 10)
423+
blk10, err := chain.Repo().NewBestChain().GetBlockSummary(10)
424+
if err != nil {
425+
t.Fatalf("failed to get block 10: %v", err)
426+
}
427+
err = pruner.pruneTries(chain.Repo().NewChain(blk10.Header.ID()), 0, blk10.Header.Number())
428+
assert.NoError(t, err)
429+
430+
st := chain.State()
431+
balance, err := st.GetEnergy(to, chain.Repo().BestBlockSummary().Header.Timestamp(), math.MaxUint64)
432+
assert.NoError(t, err)
433+
assert.Equal(t, tc.expectedEnergy, balance.Uint64())
434+
435+
st = state.NewStater(chain.Database()).NewState(blk10.Root())
436+
_, err = st.GetEnergy(to, blk10.Header.Timestamp(), math.MaxUint64)
437+
assert.NoError(t, err)
438+
assert.Equal(t, tc.expectedEnergy, balance.Uint64())
439+
440+
sum, err := chain.Repo().NewBestChain().GetBlockSummary(9)
441+
assert.NoError(t, err)
442+
443+
st = state.NewStater(chain.Database()).NewState(sum.Root())
444+
_, err = st.GetEnergy(to, sum.Header.Timestamp(), math.MaxUint64)
445+
assert.Error(t, err)
446+
assert.Contains(t, err.Error(), "missing trie node")
447+
})
448+
}
449+
}
450+
451+
func contains(slice []int, val int) bool {
452+
return slices.Contains(slice, val)
453+
}

0 commit comments

Comments
 (0)