From 221b69c9cf76ea7cd08148dc0a06267d03f728b8 Mon Sep 17 00:00:00 2001
From: Darren Kelly <107671032+darrenvechain@users.noreply.github.com>
Date: Thu, 21 Nov 2024 09:15:56 +0000
Subject: [PATCH 01/15] fix(documentation): use absolute links in markdown
(#889)
---
README.md | 14 +++++++-------
api/doc/README.md | 4 ++--
docs/CONTRIBUTING.md | 2 +-
docs/hosting-a-node.md | 2 +-
docs/usage.md | 4 ++--
5 files changed, 13 insertions(+), 13 deletions(-)
diff --git a/README.md b/README.md
index 50ee4e85c..c41d0923a 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,8 @@
@@ -44,9 +44,9 @@ ___
## Documentation
-- [Build](./docs/build.md) - How to build the `thor` binary.
-- [Usage](./docs/usage.md) - How to run thor with different configurations.
-- [Hosting a Node](./docs/hosting-a-node.md) - Considerations and requirements for hosting a node.
+- [Build](https://github.com/vechain/thor/blob/master/docs/build.md) - How to build the `thor` binary.
+- [Usage](https://github.com/vechain/thor/blob/master/docs/usage.md) - How to run thor with different configurations.
+- [Hosting a Node](https://github.com/vechain/thor/blob/master/docs/hosting-a-node.md) - Considerations and requirements for hosting a node.
- [Core Concepts](https://docs.vechain.org/core-concepts) - Core concepts of the VeChainThor blockchain.
- [API Reference](https://mainnet.vechain.org) - The API reference for the VeChainThor blockchain.
@@ -67,7 +67,7 @@ To chat with other community members you can join:
-Do note that our [Code of Conduct](./docs/CODE_OF_CONDUCT.md) applies to all VeChain community channels. Users are
+Do note that our [Code of Conduct](https://github.com/vechain/thor/blob/master/docs/CODE_OF_CONDUCT.md) applies to all VeChain community channels. Users are
**highly encouraged** to read and adhere to them to avoid repercussions.
---
@@ -75,7 +75,7 @@ Do note that our [Code of Conduct](./docs/CODE_OF_CONDUCT.md) applies to all VeC
## Contributing
Contributions to VeChainThor are welcome and highly appreciated. However, before you jump right into it, we would like
-you to review our [Contribution Guidelines](./docs/CONTRIBUTING.md) to make sure you have a smooth experience
+you to review our [Contribution Guidelines](https://github.com/vechain/thor/blob/master/docs/CONTRIBUTING.md) to make sure you have a smooth experience
contributing to VeChainThor.
---
diff --git a/api/doc/README.md b/api/doc/README.md
index 2f2c0ee62..644641053 100644
--- a/api/doc/README.md
+++ b/api/doc/README.md
@@ -1,7 +1,7 @@
## Swagger
swagger-ui from https://github.com/swagger-api/swagger-ui @v5.11.2
-- Created [window-observer.js](swagger-ui/window-observer.js) to remove `Try it out` functionality for subscription endpoints
+- Created [window-observer.js](./swagger-ui/window-observer.js) to remove `Try it out` functionality for subscription endpoints
```bash
curl https://unpkg.com/swagger-ui-dist@5.11.2/swagger-ui.css > swagger-ui/swagger-ui.css
@@ -11,7 +11,7 @@ curl https://unpkg.com/swagger-ui-dist@5.11.2/swagger-ui-standalone-preset.js >
## Stoplight
Spotlight UI from https://github.com/stoplightio/elements @v8.0.3
-- Created [window-observer.js](stoplight-ui/window-observer.js) to remove `Send API Request` functionality for subscription endpoints
+- Created [window-observer.js](./stoplight-ui/window-observer.js) to remove `Send API Request` functionality for subscription endpoints
```bash
curl https://unpkg.com/@stoplight/elements@8.0.3/styles.min.css > stoplight-ui/styles.min.css
diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md
index 407dc0b2a..e5a45b559 100644
--- a/docs/CONTRIBUTING.md
+++ b/docs/CONTRIBUTING.md
@@ -1,7 +1,7 @@
# Contributing to VechainThor
Welcome to VechainThor! We appreciate your interest in contributing. By participating in this project, you agree to
-abide by our [Code of Conduct](https://github.com/vechain/thor/blob/master/CODE_OF_CONDUCT.md).
+abide by our [Code of Conduct](https://github.com/vechain/thor/blob/master/docs/CODE_OF_CONDUCT.md).
## VeChain Improvement Proposals (VIPs)
diff --git a/docs/hosting-a-node.md b/docs/hosting-a-node.md
index 1dc1e12ce..6212d7360 100644
--- a/docs/hosting-a-node.md
+++ b/docs/hosting-a-node.md
@@ -21,7 +21,7 @@ state, including the disk space required for various node types.
### Command Line Options
-Please refer to [Command Line Options](./usage.md#command-line-options) in the usage documentation to see a list of all
+Please refer to [Command Line Options](https://github.com/vechain/thor/blob/master/docs/usage.md#command-line-options) in the usage documentation to see a list of all
available options.
---
diff --git a/docs/usage.md b/docs/usage.md
index 7358e3f0e..3a3b7694a 100644
--- a/docs/usage.md
+++ b/docs/usage.md
@@ -20,7 +20,7 @@ ___
### Running from source
-- To install the `thor` binary, follow the instructions in the [build](build) guide.
+- To install the `thor` binary, follow the instructions in the [build](https://github.com/vechain/thor/blob/master/docs/build.md) guide.
Connect to vechain's mainnet:
@@ -47,7 +47,7 @@ ___
### Running a discovery node
-- To install the `disco` binary, follow the instructions in the [build](build) guide.
+- To install the `disco` binary, follow the instructions in the [build](https://github.com/vechain/thor/blob/master/docs/build.md) guide.
Start a discovery node:
From ae97fba2f93abc76050306b0fa2565c766f3b205 Mon Sep 17 00:00:00 2001
From: Pedro Gomes
Date: Mon, 25 Nov 2024 15:42:44 +0000
Subject: [PATCH 02/15] Add benchmark test to node block process (#892)
* Add benchmark test to node block process
* added file-based storage
* use tempdir
---
cmd/thor/node/node_benchmark_test.go | 547 +++++++++++++++++++++++++++
test/testchain/chain.go | 9 +-
2 files changed, 555 insertions(+), 1 deletion(-)
create mode 100644 cmd/thor/node/node_benchmark_test.go
diff --git a/cmd/thor/node/node_benchmark_test.go b/cmd/thor/node/node_benchmark_test.go
new file mode 100644
index 000000000..0f6d1f1f0
--- /dev/null
+++ b/cmd/thor/node/node_benchmark_test.go
@@ -0,0 +1,547 @@
+// Copyright (c) 2024 The VeChainThor developers
+
+// Distributed under the GNU Lesser General Public License v3.0 software license, see the accompanying
+// file LICENSE or
+
+package node
+
+import (
+ "crypto/ecdsa"
+ "crypto/rand"
+ "fmt"
+ "math"
+ "math/big"
+ "path/filepath"
+ "runtime/debug"
+ "sync"
+ "testing"
+
+ "github.com/elastic/gosigar"
+ "github.com/ethereum/go-ethereum/common/fdlimit"
+ "github.com/ethereum/go-ethereum/crypto"
+ "github.com/pkg/errors"
+ "github.com/stretchr/testify/require"
+ "github.com/vechain/thor/v2/bft"
+ "github.com/vechain/thor/v2/block"
+ "github.com/vechain/thor/v2/chain"
+ "github.com/vechain/thor/v2/cmd/thor/solo"
+ "github.com/vechain/thor/v2/genesis"
+ "github.com/vechain/thor/v2/logdb"
+ "github.com/vechain/thor/v2/muxdb"
+ "github.com/vechain/thor/v2/packer"
+ "github.com/vechain/thor/v2/state"
+ "github.com/vechain/thor/v2/test/datagen"
+ "github.com/vechain/thor/v2/test/testchain"
+ "github.com/vechain/thor/v2/thor"
+ "github.com/vechain/thor/v2/tx"
+)
+
+var (
+ cachedAccounts []genesis.DevAccount
+ once sync.Once
+ blockCount = 1_000
+)
+
+func getCachedAccounts(b *testing.B) []genesis.DevAccount {
+ once.Do(func() {
+ cachedAccounts = createAccounts(b, 1_000)
+ })
+ return cachedAccounts
+}
+
+func BenchmarkBlockProcess_RandomSigners_ManyClausesPerTx_RealDB(b *testing.B) {
+ // create state accounts
+ accounts := getCachedAccounts(b)
+
+ // randomly pick a signer for signing the transactions
+ randomSignerFunc := randomPickSignerFunc(accounts, createOneClausePerTx)
+
+ // create blocks
+ blocks := createBlocks(b, blockCount, accounts, randomSignerFunc)
+
+ // create test db - will be automagically removed when the benchmark ends
+ db, err := openTempMainDB(b.TempDir())
+ require.NoError(b, err)
+
+ // run the benchmark
+ benchmarkBlockProcess(b, db, accounts, blocks)
+}
+func BenchmarkBlockProcess_RandomSigners_OneClausePerTx_RealDB(b *testing.B) {
+ // create state accounts
+ accounts := getCachedAccounts(b)
+
+ // randomly pick a signer for signing the transactions
+ randomSignerFunc := randomPickSignerFunc(accounts, createManyClausesPerTx)
+
+ // create blocks
+ blocks := createBlocks(b, blockCount, accounts, randomSignerFunc)
+
+ // create test db - will be automagically removed when the benchmark ends
+ db, err := openTempMainDB(b.TempDir())
+ require.NoError(b, err)
+
+ // run the benchmark
+ benchmarkBlockProcess(b, db, accounts, blocks)
+}
+func BenchmarkBlockProcess_ManyClausesPerTx_RealDB(b *testing.B) {
+ // create state accounts
+ accounts := getCachedAccounts(b)
+
+ // Use one signer for signing the transactions
+ singleSignerFun := randomPickSignerFunc([]genesis.DevAccount{accounts[0]}, createManyClausesPerTx)
+
+ // create blocks
+ blocks := createBlocks(b, blockCount, accounts, singleSignerFun)
+
+ // create test db - will be automagically removed when the benchmark ends
+ db, err := openTempMainDB(b.TempDir())
+ require.NoError(b, err)
+
+ // run the benchmark
+ benchmarkBlockProcess(b, db, accounts, blocks)
+}
+func BenchmarkBlockProcess_OneClausePerTx_RealDB(b *testing.B) {
+ // create state accounts
+ accounts := getCachedAccounts(b)
+
+ // Use one signer for signing the transactions
+ singleSignerFun := randomPickSignerFunc([]genesis.DevAccount{accounts[0]}, createOneClausePerTx)
+
+ // create blocks
+ blocks := createBlocks(b, blockCount, accounts, singleSignerFun)
+
+ // create test db - will be automagically removed when the benchmark ends
+ db, err := openTempMainDB(b.TempDir())
+ require.NoError(b, err)
+
+ // run the benchmark
+ benchmarkBlockProcess(b, db, accounts, blocks)
+}
+
+func BenchmarkBlockProcess_RandomSigners_ManyClausesPerTx(b *testing.B) {
+ // create state accounts
+ accounts := getCachedAccounts(b)
+
+ // randomly pick a signer for signing the transactions
+ randomSignerFunc := randomPickSignerFunc(accounts, createOneClausePerTx)
+
+ // create blocks
+ blocks := createBlocks(b, blockCount, accounts, randomSignerFunc)
+
+ // create test db
+ db := muxdb.NewMem()
+
+ // run the benchmark
+ benchmarkBlockProcess(b, db, accounts, blocks)
+}
+
+func BenchmarkBlockProcess_RandomSigners_OneClausePerTx(b *testing.B) {
+ // create state accounts
+ accounts := getCachedAccounts(b)
+
+ // randomly pick a signer for signing the transactions
+ randomSignerFunc := randomPickSignerFunc(accounts, createManyClausesPerTx)
+
+ // create blocks
+ blocks := createBlocks(b, blockCount, accounts, randomSignerFunc)
+
+ // create test db
+ db := muxdb.NewMem()
+
+ // run the benchmark
+ benchmarkBlockProcess(b, db, accounts, blocks)
+}
+
+func BenchmarkBlockProcess_ManyClausesPerTx(b *testing.B) {
+ // create state accounts
+ accounts := getCachedAccounts(b)
+
+ // Use one signer for signing the transactions
+ singleSignerFun := randomPickSignerFunc([]genesis.DevAccount{accounts[0]}, createManyClausesPerTx)
+
+ // create blocks
+ blocks := createBlocks(b, blockCount, accounts, singleSignerFun)
+
+ // create test db
+ db := muxdb.NewMem()
+
+ // run the benchmark
+ benchmarkBlockProcess(b, db, accounts, blocks)
+}
+
+func BenchmarkBlockProcess_OneClausePerTx(b *testing.B) {
+ // create state accounts
+ accounts := getCachedAccounts(b)
+
+ // Use one signer for signing the transactions
+ singleSignerFun := randomPickSignerFunc([]genesis.DevAccount{accounts[0]}, createOneClausePerTx)
+
+ // create blocks
+ blocks := createBlocks(b, blockCount, accounts, singleSignerFun)
+
+ // create test db
+ db := muxdb.NewMem()
+
+ // run the benchmark
+ benchmarkBlockProcess(b, db, accounts, blocks)
+}
+
+func benchmarkBlockProcess(b *testing.B, db *muxdb.MuxDB, accounts []genesis.DevAccount, blocks []*block.Block) {
+ // Initialize the test chain and dependencies
+ thorChain, err := createChain(db, accounts)
+ require.NoError(b, err)
+
+ proposer := &accounts[0]
+
+ engine, err := bft.NewEngine(thorChain.Repo(), thorChain.Database(), thorChain.GetForkConfig(), proposer.Address)
+ require.NoError(b, err)
+
+ node := New(
+ &Master{
+ PrivateKey: proposer.PrivateKey,
+ },
+ thorChain.Repo(),
+ engine,
+ thorChain.Stater(),
+ nil,
+ nil,
+ "",
+ nil,
+ 10_000_000,
+ true,
+ thor.NoFork,
+ )
+
+ stats := &blockStats{}
+
+ // Measure memory usage
+ b.ReportAllocs()
+
+ // Benchmark execution
+ b.ResetTimer()
+ for _, blk := range blocks {
+ _, err = node.processBlock(blk, stats)
+ if err != nil {
+ b.Fatalf("processBlock failed: %v", err)
+ }
+ }
+}
+
+func createBlocks(b *testing.B, noBlocks int, accounts []genesis.DevAccount, createTxFunc func(chain *testchain.Chain) (tx.Transactions, error)) []*block.Block {
+ proposer := &accounts[0]
+
+ // mock a fake chain for block production
+ fakeChain, err := createChain(muxdb.NewMem(), accounts)
+ require.NoError(b, err)
+
+ // pre-alloc blocks
+ var blocks []*block.Block
+ var transactions tx.Transactions
+
+ // Start from the Genesis block
+ previousBlock := fakeChain.GenesisBlock()
+ for i := 0; i < noBlocks; i++ {
+ transactions, err = createTxFunc(fakeChain)
+ require.NoError(b, err)
+ previousBlock, err = packTxsIntoBlock(
+ fakeChain,
+ proposer,
+ previousBlock,
+ transactions,
+ )
+ require.NoError(b, err)
+ blocks = append(blocks, previousBlock)
+ }
+
+ return blocks
+}
+
+func createOneClausePerTx(signerPK *ecdsa.PrivateKey, thorChain *testchain.Chain) (tx.Transactions, error) {
+ var transactions tx.Transactions
+ gasUsed := uint64(0)
+ for gasUsed < 9_500_000 {
+ toAddr := datagen.RandAddress()
+ cla := tx.NewClause(&toAddr).WithValue(big.NewInt(10000))
+ transaction := new(tx.Builder).
+ ChainTag(thorChain.Repo().ChainTag()).
+ GasPriceCoef(1).
+ Expiration(math.MaxUint32 - 1).
+ Gas(21_000).
+ Nonce(uint64(datagen.RandInt())).
+ Clause(cla).
+ BlockRef(tx.NewBlockRef(0)).
+ Build()
+
+ sig, err := crypto.Sign(transaction.SigningHash().Bytes(), signerPK)
+ if err != nil {
+ return nil, err
+ }
+ transaction = transaction.WithSignature(sig)
+
+ gasUsed += 21_000 // Gas per transaction
+ transactions = append(transactions, transaction)
+ }
+ return transactions, nil
+}
+
+func createManyClausesPerTx(signerPK *ecdsa.PrivateKey, thorChain *testchain.Chain) (tx.Transactions, error) {
+ var transactions tx.Transactions
+ gasUsed := uint64(0)
+ txGas := uint64(42_000)
+
+ transactionBuilder := new(tx.Builder).
+ ChainTag(thorChain.Repo().ChainTag()).
+ GasPriceCoef(1).
+ Expiration(math.MaxUint32 - 1).
+ Nonce(uint64(datagen.RandInt())).
+ BlockRef(tx.NewBlockRef(0))
+
+ for ; gasUsed < 9_500_000; gasUsed += txGas {
+ toAddr := datagen.RandAddress()
+ transactionBuilder.Clause(tx.NewClause(&toAddr).WithValue(big.NewInt(10000)))
+ }
+
+ transaction := transactionBuilder.Gas(gasUsed).Build()
+
+ sig, err := crypto.Sign(transaction.SigningHash().Bytes(), signerPK)
+ if err != nil {
+ return nil, err
+ }
+ transaction = transaction.WithSignature(sig)
+
+ transactions = append(transactions, transaction)
+
+ return transactions, nil
+}
+
+func packTxsIntoBlock(thorChain *testchain.Chain, proposerAccount *genesis.DevAccount, parentBlk *block.Block, transactions tx.Transactions) (*block.Block, error) {
+ p := packer.New(thorChain.Repo(), thorChain.Stater(), proposerAccount.Address, &proposerAccount.Address, thorChain.GetForkConfig())
+
+ parentSum, err := thorChain.Repo().GetBlockSummary(parentBlk.Header().ID())
+ if err != nil {
+ return nil, err
+ }
+
+ flow, err := p.Schedule(parentSum, parentBlk.Header().Timestamp()+1)
+ if err != nil {
+ return nil, err
+ }
+
+ for _, transaction := range transactions {
+ err = flow.Adopt(transaction)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ b1, stage, receipts, err := flow.Pack(proposerAccount.PrivateKey, 0, false)
+ if err != nil {
+ return nil, err
+ }
+
+ if _, err := stage.Commit(); err != nil {
+ return nil, err
+ }
+
+ if err := thorChain.Repo().AddBlock(b1, receipts, 0); err != nil {
+ return nil, err
+ }
+
+ if err := thorChain.Repo().SetBestBlockID(b1.Header().ID()); err != nil {
+ return nil, err
+ }
+
+ return b1, nil
+}
+
+func createChain(db *muxdb.MuxDB, accounts []genesis.DevAccount) (*testchain.Chain, error) {
+ forkConfig := thor.NoFork
+ forkConfig.VIP191 = 1
+ forkConfig.BLOCKLIST = 0
+ forkConfig.VIP214 = 2
+
+ // Create the state manager (Stater) with the initialized database.
+ stater := state.NewStater(db)
+
+ authAccs := make([]genesis.Authority, 0, len(accounts))
+ stateAccs := make([]genesis.Account, 0, len(accounts))
+
+ for _, acc := range accounts {
+ authAccs = append(authAccs, genesis.Authority{
+ MasterAddress: acc.Address,
+ EndorsorAddress: acc.Address,
+ Identity: thor.BytesToBytes32([]byte("master")),
+ })
+ bal, _ := new(big.Int).SetString("1000000000000000000000000000", 10)
+ stateAccs = append(stateAccs, genesis.Account{
+ Address: acc.Address,
+ Balance: (*genesis.HexOrDecimal256)(bal),
+ Energy: (*genesis.HexOrDecimal256)(bal),
+ Code: "",
+ Storage: nil,
+ })
+ }
+ mbp := uint64(1_000)
+ genConfig := genesis.CustomGenesis{
+ LaunchTime: 1526400000,
+ GasLimit: thor.InitialGasLimit,
+ ExtraData: "",
+ ForkConfig: &forkConfig,
+ Authority: authAccs,
+ Accounts: stateAccs,
+ Params: genesis.Params{
+ MaxBlockProposers: &mbp,
+ },
+ }
+
+ builder, err := genesis.NewCustomNet(&genConfig)
+ if err != nil {
+ return nil, err
+ }
+
+ // Initialize the genesis and retrieve the genesis block
+ //gene := genesis.NewDevnet()
+ geneBlk, _, _, err := builder.Build(stater)
+ if err != nil {
+ return nil, err
+ }
+
+ // Create the repository which manages chain data, using the database and genesis block.
+ repo, err := chain.NewRepository(db, geneBlk)
+ if err != nil {
+ return nil, err
+ }
+
+ // Create an inMemory logdb
+ logDb, err := logdb.NewMem()
+ if err != nil {
+ return nil, err
+ }
+
+ return testchain.New(
+ db,
+ builder,
+ solo.NewBFTEngine(repo),
+ repo,
+ stater,
+ geneBlk,
+ logDb,
+ thor.NoFork,
+ ), nil
+}
+
+func randomPickSignerFunc(
+ accounts []genesis.DevAccount,
+ createTxFun func(signerPK *ecdsa.PrivateKey, thorChain *testchain.Chain) (tx.Transactions, error),
+) func(chain *testchain.Chain) (tx.Transactions, error) {
+ return func(chain *testchain.Chain) (tx.Transactions, error) {
+ // Ensure there are accounts available
+ if len(accounts) == 0 {
+ return nil, fmt.Errorf("no accounts available to pick a random sender")
+ }
+
+ // Securely pick a random index
+ maxLen := big.NewInt(int64(len(accounts)))
+ randomIndex, err := rand.Int(rand.Reader, maxLen)
+ if err != nil {
+ return nil, fmt.Errorf("failed to generate random index: %v", err)
+ }
+
+ // Use the selected account to create transactions
+ sender := accounts[randomIndex.Int64()]
+ return createTxFun(sender.PrivateKey, chain)
+ }
+}
+
+func createAccounts(b *testing.B, accountNo int) []genesis.DevAccount {
+ var accs []genesis.DevAccount
+
+ for i := 0; i < accountNo; i++ {
+ pk, err := crypto.GenerateKey()
+ require.NoError(b, err)
+ addr := crypto.PubkeyToAddress(pk.PublicKey)
+ accs = append(accs, genesis.DevAccount{Address: thor.Address(addr), PrivateKey: pk})
+ }
+
+ return accs
+}
+
+func openTempMainDB(dir string) (*muxdb.MuxDB, error) {
+ cacheMB := normalizeCacheSize(4096)
+
+ fdCache := suggestFDCache()
+
+ opts := muxdb.Options{
+ TrieNodeCacheSizeMB: cacheMB,
+ TrieRootCacheCapacity: 256,
+ TrieCachedNodeTTL: 30, // 5min
+ TrieLeafBankSlotCapacity: 256,
+ TrieDedupedPartitionFactor: math.MaxUint32,
+ TrieWillCleanHistory: true,
+ OpenFilesCacheCapacity: fdCache,
+ ReadCacheMB: 256, // rely on os page cache other than huge db read cache.
+ WriteBufferMB: 128,
+ }
+
+ // go-ethereum stuff
+ // Ensure Go's GC ignores the database cache for trigger percentage
+ totalCacheMB := cacheMB + opts.ReadCacheMB + opts.WriteBufferMB*2
+ gogc := math.Max(10, math.Min(100, 50/(float64(totalCacheMB)/1024)))
+
+ debug.SetGCPercent(int(gogc))
+
+ if opts.TrieWillCleanHistory {
+ opts.TrieHistPartitionFactor = 1000
+ } else {
+ opts.TrieHistPartitionFactor = 500000
+ }
+
+ db, err := muxdb.Open(filepath.Join(dir, "maindb"), &opts)
+ if err != nil {
+ return nil, errors.Wrapf(err, "open main database [%v]", dir)
+ }
+ return db, nil
+}
+
+func normalizeCacheSize(sizeMB int) int {
+ if sizeMB < 128 {
+ sizeMB = 128
+ }
+
+ var mem gosigar.Mem
+ if err := mem.Get(); err != nil {
+ fmt.Println("failed to get total mem:", "err", err)
+ } else {
+ total := int(mem.Total / 1024 / 1024)
+ half := total / 2
+
+ // limit to not less than total/2 and up to total-2GB
+ limitMB := total - 2048
+ if limitMB < half {
+ limitMB = half
+ }
+
+ if sizeMB > limitMB {
+ sizeMB = limitMB
+ fmt.Println("cache size(MB) limited", "limit", limitMB)
+ }
+ }
+ return sizeMB
+}
+
+func suggestFDCache() int {
+ limit, err := fdlimit.Current()
+ if err != nil {
+ fmt.Println("unable to get fdlimit", "error", err)
+ return 500
+ }
+ if limit <= 1024 {
+ fmt.Println("low fd limit, increase it if possible", "limit", limit)
+ }
+
+ n := limit / 2
+ if n > 5120 {
+ return 5120
+ }
+ return n
+}
diff --git a/test/testchain/chain.go b/test/testchain/chain.go
index b35687d14..af908594e 100644
--- a/test/testchain/chain.go
+++ b/test/testchain/chain.go
@@ -46,6 +46,7 @@ func New(
stater *state.Stater,
genesisBlock *block.Block,
logDB *logdb.LogDB,
+ forkConfig thor.ForkConfig,
) *Chain {
return &Chain{
db: db,
@@ -55,7 +56,7 @@ func New(
stater: stater,
genesisBlock: genesisBlock,
logDB: logDB,
- forkConfig: thor.GetForkConfig(genesisBlock.Header().ID()),
+ forkConfig: forkConfig,
}
}
@@ -87,6 +88,11 @@ func NewIntegrationTestChain() (*Chain, error) {
return nil, err
}
+ forkConfig := thor.NoFork
+ forkConfig.VIP191 = 1
+ forkConfig.BLOCKLIST = 0
+ forkConfig.VIP214 = 2
+
return New(
db,
gene,
@@ -95,6 +101,7 @@ func NewIntegrationTestChain() (*Chain, error) {
stater,
geneBlk,
logDb,
+ thor.NoFork,
), nil
}
From 198e537cf46243de4e505c822bfebd442a33d2b5 Mon Sep 17 00:00:00 2001
From: libotony
Date: Wed, 27 Nov 2024 15:31:07 +0800
Subject: [PATCH 03/15] update dependency go-ethereum (#895)
---
go.mod | 14 +++++++-------
go.sum | 30 +++++++++++++++++-------------
2 files changed, 24 insertions(+), 20 deletions(-)
diff --git a/go.mod b/go.mod
index 4ba00ec9a..f3032a927 100644
--- a/go.mod
+++ b/go.mod
@@ -18,7 +18,7 @@ require (
github.com/mattn/go-sqlite3 v1.14.22
github.com/mattn/go-tty v0.0.0-20180219170247-931426f7535a
github.com/pborman/uuid v0.0.0-20170612153648-e790cca94e6c
- github.com/pkg/errors v0.8.0
+ github.com/pkg/errors v0.8.1-0.20171216070316-e881fd58d78e
github.com/pmezard/go-difflib v1.0.0
github.com/prometheus/client_golang v1.18.0
github.com/prometheus/client_model v0.5.0
@@ -27,7 +27,7 @@ require (
github.com/stretchr/testify v1.8.4
github.com/syndtr/goleveldb v1.0.1-0.20220614013038-64ee5596c38a
github.com/vechain/go-ecvrf v0.0.0-20220525125849-96fa0442e765
- golang.org/x/crypto v0.21.0
+ golang.org/x/crypto v0.22.0
gopkg.in/cheggaaa/pb.v1 v1.0.28
gopkg.in/urfave/cli.v1 v1.20.0
gopkg.in/yaml.v3 v3.0.1
@@ -37,6 +37,7 @@ require (
github.com/aristanetworks/goarista v0.0.0-20180222005525-c41ed3986faa // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/btcsuite/btcd v0.0.0-20171128150713-2e60448ffcc6 // indirect
+ github.com/btcsuite/btcd/btcec/v2 v2.3.4 // indirect
github.com/cespare/cp v1.1.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/deckarep/golang-set v1.7.1 // indirect
@@ -46,17 +47,16 @@ require (
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
github.com/go-stack/stack v1.7.0 // indirect
github.com/golang/snappy v0.0.4 // indirect
- github.com/google/go-cmp v0.6.0 // indirect
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect
github.com/huin/goupnp v0.0.0-20171109214107-dceda08e705b // indirect
- github.com/jackpal/go-nat-pmp v1.0.1 // indirect
+ github.com/jackpal/go-nat-pmp v1.0.2-0.20160603034137-1fa385a6f458 // indirect
github.com/mattn/go-colorable v0.0.9 // indirect
github.com/mattn/go-runewidth v0.0.4 // indirect
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
github.com/rjeczalik/notify v0.9.3 // indirect
- golang.org/x/net v0.23.0 // indirect
- golang.org/x/sys v0.18.0 // indirect
+ golang.org/x/net v0.24.0 // indirect
+ golang.org/x/sys v0.19.0 // indirect
golang.org/x/text v0.14.0 // indirect
google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/karalabe/cookiejar.v2 v2.0.0-20150724131613-8dcd6a7f4951 // indirect
@@ -64,4 +64,4 @@ require (
replace github.com/syndtr/goleveldb => github.com/vechain/goleveldb v1.0.1-0.20220809091043-51eb019c8655
-replace github.com/ethereum/go-ethereum => github.com/vechain/go-ethereum v1.8.15-0.20240528020007-2994c2a24b9c
+replace github.com/ethereum/go-ethereum => github.com/vechain/go-ethereum v1.8.15-0.20241126085506-c74017ec91b2
diff --git a/go.sum b/go.sum
index fd12a92aa..648890cc0 100644
--- a/go.sum
+++ b/go.sum
@@ -6,6 +6,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/btcsuite/btcd v0.0.0-20171128150713-2e60448ffcc6 h1:Eey/GGQ/E5Xp1P2Lyx1qj007hLZfbi0+CoVeJruGCtI=
github.com/btcsuite/btcd v0.0.0-20171128150713-2e60448ffcc6/go.mod h1:Dmm/EzmjnCiweXmzRIAiUWCInVmPgjkzgv5k4tVyXiQ=
+github.com/btcsuite/btcd/btcec/v2 v2.3.4 h1:3EJjcN70HCu/mwqlUsGK8GcNVyLVxFDlWurTXGPFfiQ=
+github.com/btcsuite/btcd/btcec/v2 v2.3.4/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04=
github.com/cespare/cp v1.1.1 h1:nCb6ZLdB7NRaqsm91JtQTAme2SKJzXVsdPIPkyJr1MU=
github.com/cespare/cp v1.1.1/go.mod h1:SOGHArjBr4JWaSDEVpWpo/hNg6RoKrls6Oh40hiwW+s=
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
@@ -23,6 +25,7 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/deckarep/golang-set v1.7.1 h1:SCQV0S6gTtp6itiFrTqI+pfmJ4LN85S1YzhDf9rTHJQ=
github.com/deckarep/golang-set v1.7.1/go.mod h1:93vsz/8Wt4joVM7c2AVqh+YRMiUSc14yDtF28KmMOgQ=
+github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0=
github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs=
@@ -84,8 +87,8 @@ github.com/huin/goupnp v0.0.0-20171109214107-dceda08e705b h1:mvnS3LbcRgdM4nBLksE
github.com/huin/goupnp v0.0.0-20171109214107-dceda08e705b/go.mod h1:MZ2ZmwcBpvOoJ22IJsc7va19ZwoheaBk43rKg12SKag=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
-github.com/jackpal/go-nat-pmp v1.0.1 h1:i0LektDkO1QlrTm/cSuP+PyBCDnYvjPLGl4LdWEMiaA=
-github.com/jackpal/go-nat-pmp v1.0.1/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
+github.com/jackpal/go-nat-pmp v1.0.2-0.20160603034137-1fa385a6f458 h1:6OvNmYgJyexcZ3pYbTI9jWx5tHo1Dee/tWbLMfPe2TA=
+github.com/jackpal/go-nat-pmp v1.0.2-0.20160603034137-1fa385a6f458/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
@@ -119,12 +122,13 @@ github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
-github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw=
github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro=
+github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk=
+github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0=
github.com/pborman/uuid v0.0.0-20170612153648-e790cca94e6c h1:MUyE44mTvnI5A0xrxIxaMqoWFzPfQvtE2IWUollMDMs=
github.com/pborman/uuid v0.0.0-20170612153648-e790cca94e6c/go.mod h1:VyrYX9gd7irzKovcSS6BIIEwPRkP2Wm2m9ufcdFSJ34=
-github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
-github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.8.1-0.20171216070316-e881fd58d78e h1:osn9cOzd93npXpRuTFR/MPjiTvTSNHA7pqbXkPyLqQ4=
+github.com/pkg/errors v0.8.1-0.20171216070316-e881fd58d78e/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk=
@@ -150,8 +154,8 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/vechain/go-ecvrf v0.0.0-20220525125849-96fa0442e765 h1:jvr+TSivjObZmOKVdqlgeLtRhaDG27gE39PMuE2IJ24=
github.com/vechain/go-ecvrf v0.0.0-20220525125849-96fa0442e765/go.mod h1:cwnTMgAVzMb30xMKnGI1LdU1NjMiPllYb7i3ibj/fzE=
-github.com/vechain/go-ethereum v1.8.15-0.20240528020007-2994c2a24b9c h1:YfGsGXMNKI64gR76KumYgGnYSdAFtMA8igtmpFiBt74=
-github.com/vechain/go-ethereum v1.8.15-0.20240528020007-2994c2a24b9c/go.mod h1:EhX+lSkpNdEIxu1zOXtiFZu5nv1i8MX1mQA/qhUE+gw=
+github.com/vechain/go-ethereum v1.8.15-0.20241126085506-c74017ec91b2 h1:ch3DqXvl1ApfJut768bf5Vlhqtw+bxAWTyPDYXQkQZk=
+github.com/vechain/go-ethereum v1.8.15-0.20241126085506-c74017ec91b2/go.mod h1:yPUCNmntAh1PritrMfSi7noK+9vVPStZX3wgh3ieaY0=
github.com/vechain/goleveldb v1.0.1-0.20220809091043-51eb019c8655 h1:CbHcWpCi7wOYfpoErRABh3Slyq9vO0Ay/EHN5GuJSXQ=
github.com/vechain/goleveldb v1.0.1-0.20220809091043-51eb019c8655/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -160,8 +164,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
-golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
+golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
+golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -174,8 +178,8 @@ golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
-golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
-golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
+golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
+golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -199,8 +203,8 @@ golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
-golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
+golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
From 45ab2bde4c6af9b8e06453c54b02663e95232f0a Mon Sep 17 00:00:00 2001
From: Darren Kelly <107671032+darrenvechain@users.noreply.github.com>
Date: Fri, 29 Nov 2024 09:35:32 +0000
Subject: [PATCH 04/15] chore: update API metrics bucket and endpoint names
(#893)
* chore: update API metrics bucket and endpoint names
* fix: typo & tests
* fix: lint
* chore: add websocket total counter
* fix: txs endpoints names & ws subject
* fix: unit tests
* chore: standardise naming convention
* chore: add websocke duration & http code
* chore: add websocke duration & http code
* fix: lint issues
* fix: sync issues with metrics
* chore: update websocket durations bucket
* fix: PR comments - use sync.Once
---
api/accounts/accounts.go | 12 ++++++------
api/blocks/blocks.go | 2 +-
api/debug/debug.go | 6 +++---
api/events/events.go | 2 +-
api/metrics.go | 28 +++++++++++++++++-----------
api/metrics_test.go | 22 +++++++++++-----------
api/node/node.go | 2 +-
api/transactions/transactions.go | 6 +++---
api/transfers/transfers.go | 2 +-
metrics/telemetry.go | 18 ++++++++++++------
10 files changed, 56 insertions(+), 44 deletions(-)
diff --git a/api/accounts/accounts.go b/api/accounts/accounts.go
index 4bd940709..54058a160 100644
--- a/api/accounts/accounts.go
+++ b/api/accounts/accounts.go
@@ -358,27 +358,27 @@ func (a *Accounts) Mount(root *mux.Router, pathPrefix string) {
sub.Path("/*").
Methods(http.MethodPost).
- Name("accounts_call_batch_code").
+ Name("POST /accounts/*").
HandlerFunc(utils.WrapHandlerFunc(a.handleCallBatchCode))
sub.Path("/{address}").
Methods(http.MethodGet).
- Name("accounts_get_account").
+ Name("GET /accounts/{address}").
HandlerFunc(utils.WrapHandlerFunc(a.handleGetAccount))
sub.Path("/{address}/code").
Methods(http.MethodGet).
- Name("accounts_get_code").
+ Name("GET /accounts/{address}/code").
HandlerFunc(utils.WrapHandlerFunc(a.handleGetCode))
sub.Path("/{address}/storage/{key}").
Methods("GET").
- Name("accounts_get_storage").
+ Name("GET /accounts/{address}/storage").
HandlerFunc(utils.WrapHandlerFunc(a.handleGetStorage))
// These two methods are currently deprecated
sub.Path("").
Methods(http.MethodPost).
- Name("accounts_call_contract").
+ Name("POST /accounts").
HandlerFunc(utils.WrapHandlerFunc(a.handleCallContract))
sub.Path("/{address}").
Methods(http.MethodPost).
- Name("accounts_call_contract_address").
+ Name("POST /accounts/{address}").
HandlerFunc(utils.WrapHandlerFunc(a.handleCallContract))
}
diff --git a/api/blocks/blocks.go b/api/blocks/blocks.go
index bddb3ac12..ff86e02e6 100644
--- a/api/blocks/blocks.go
+++ b/api/blocks/blocks.go
@@ -95,6 +95,6 @@ func (b *Blocks) Mount(root *mux.Router, pathPrefix string) {
sub := root.PathPrefix(pathPrefix).Subrouter()
sub.Path("/{revision}").
Methods(http.MethodGet).
- Name("blocks_get_block").
+ Name("GET /blocks/{revision}").
HandlerFunc(utils.WrapHandlerFunc(b.handleGetBlock))
}
diff --git a/api/debug/debug.go b/api/debug/debug.go
index e84a88d57..5ff54f1dc 100644
--- a/api/debug/debug.go
+++ b/api/debug/debug.go
@@ -466,14 +466,14 @@ func (d *Debug) Mount(root *mux.Router, pathPrefix string) {
sub.Path("/tracers").
Methods(http.MethodPost).
- Name("debug_trace_clause").
+ Name("POST /debug/tracers").
HandlerFunc(utils.WrapHandlerFunc(d.handleTraceClause))
sub.Path("/tracers/call").
Methods(http.MethodPost).
- Name("debug_trace_call").
+ Name("POST /debug/tracers/call").
HandlerFunc(utils.WrapHandlerFunc(d.handleTraceCall))
sub.Path("/storage-range").
Methods(http.MethodPost).
- Name("debug_trace_storage").
+ Name("POST /debug/storage-range").
HandlerFunc(utils.WrapHandlerFunc(d.handleDebugStorage))
}
diff --git a/api/events/events.go b/api/events/events.go
index 40dff7b09..669c47b03 100644
--- a/api/events/events.go
+++ b/api/events/events.go
@@ -84,6 +84,6 @@ func (e *Events) Mount(root *mux.Router, pathPrefix string) {
sub.Path("").
Methods(http.MethodPost).
- Name("logs_filter_event").
+ Name("POST /logs/event").
HandlerFunc(utils.WrapHandlerFunc(e.handleFilter))
}
diff --git a/api/metrics.go b/api/metrics.go
index 9fd5c3d94..57631be71 100644
--- a/api/metrics.go
+++ b/api/metrics.go
@@ -19,9 +19,15 @@ import (
)
var (
+ websocketDurations = []int64{
+ 0, 1, 2, 5, 10, 25, 50, 100, 250, 500, 1_000, 2_500, 5_000, 10_000, 25_000,
+ 50_000, 100_000, 250_000, 500_000, 1000_000, 2_500_000, 5_000_000, 10_000_000,
+ }
metricHTTPReqCounter = metrics.LazyLoadCounterVec("api_request_count", []string{"name", "code", "method"})
metricHTTPReqDuration = metrics.LazyLoadHistogramVec("api_duration_ms", []string{"name", "code", "method"}, metrics.BucketHTTPReqs)
- metricActiveWebsocketCount = metrics.LazyLoadGaugeVec("api_active_websocket_count", []string{"subject"})
+ metricWebsocketDuration = metrics.LazyLoadHistogramVec("api_websocket_duration", []string{"name", "code"}, websocketDurations)
+ metricActiveWebsocketGauge = metrics.LazyLoadGaugeVec("api_active_websocket_gauge", []string{"name"})
+ metricWebsocketCounter = metrics.LazyLoadCounterVec("api_websocket_counter", []string{"name"})
)
// metricsResponseWriter is a wrapper around http.ResponseWriter that captures the status code.
@@ -62,7 +68,7 @@ func metricsMiddleware(next http.Handler) http.Handler {
var (
enabled = false
name = ""
- subscription = ""
+ subscription = false
)
// all named route will be recorded
@@ -70,24 +76,24 @@ func metricsMiddleware(next http.Handler) http.Handler {
enabled = true
name = rt.GetName()
if strings.HasPrefix(name, "subscriptions") {
- // example path: /subscriptions/txpool -> subject = txpool
- paths := strings.Split(r.URL.Path, "/")
- if len(paths) > 2 {
- subscription = paths[2]
- }
+ subscription = true
+ name = "WS " + r.URL.Path
}
}
now := time.Now()
mrw := newMetricsResponseWriter(w)
- if subscription != "" {
- metricActiveWebsocketCount().AddWithLabel(1, map[string]string{"subject": subscription})
+ if subscription {
+ metricActiveWebsocketGauge().AddWithLabel(1, map[string]string{"name": name})
+ metricWebsocketCounter().AddWithLabel(1, map[string]string{"name": name})
}
next.ServeHTTP(mrw, r)
- if subscription != "" {
- metricActiveWebsocketCount().AddWithLabel(-1, map[string]string{"subject": subscription})
+ if subscription {
+ metricActiveWebsocketGauge().AddWithLabel(-1, map[string]string{"name": name})
+ // record websocket duration in seconds, not MS
+ metricWebsocketDuration().ObserveWithLabels(time.Since(now).Milliseconds()/1000, map[string]string{"name": name, "code": strconv.Itoa(mrw.statusCode)})
} else if enabled {
metricHTTPReqCounter().AddWithLabel(1, map[string]string{"name": name, "code": strconv.Itoa(mrw.statusCode), "method": r.Method})
metricHTTPReqDuration().ObserveWithLabels(time.Since(now).Milliseconds(), map[string]string{"name": name, "code": strconv.Itoa(mrw.statusCode), "method": r.Method})
diff --git a/api/metrics_test.go b/api/metrics_test.go
index 7cb1794e4..4e4b6daaf 100644
--- a/api/metrics_test.go
+++ b/api/metrics_test.go
@@ -77,7 +77,7 @@ func TestMetricsMiddleware(t *testing.T) {
assert.Equal(t, "method", labels[1].GetName())
assert.Equal(t, "GET", labels[1].GetValue())
assert.Equal(t, "name", labels[2].GetName())
- assert.Equal(t, "accounts_get_account", labels[2].GetValue())
+ assert.Equal(t, "GET /accounts/{address}", labels[2].GetValue())
labels = m[1].GetLabel()
assert.Equal(t, 3, len(labels))
@@ -86,7 +86,7 @@ func TestMetricsMiddleware(t *testing.T) {
assert.Equal(t, "method", labels[1].GetName())
assert.Equal(t, "GET", labels[1].GetValue())
assert.Equal(t, "name", labels[2].GetName())
- assert.Equal(t, "accounts_get_account", labels[2].GetValue())
+ assert.Equal(t, "GET /accounts/{address}", labels[2].GetValue())
labels = m[2].GetLabel()
assert.Equal(t, 3, len(labels))
@@ -95,7 +95,7 @@ func TestMetricsMiddleware(t *testing.T) {
assert.Equal(t, "method", labels[1].GetName())
assert.Equal(t, "GET", labels[1].GetValue())
assert.Equal(t, "name", labels[2].GetName())
- assert.Equal(t, "accounts_get_account", labels[2].GetValue())
+ assert.Equal(t, "GET /accounts/{address}", labels[2].GetValue())
}
func TestWebsocketMetrics(t *testing.T) {
@@ -120,13 +120,13 @@ func TestWebsocketMetrics(t *testing.T) {
metrics, err := parser.TextToMetricFamilies(bytes.NewReader(body))
assert.Nil(t, err)
- m := metrics["thor_metrics_api_active_websocket_count"].GetMetric()
+ m := metrics["thor_metrics_api_active_websocket_gauge"].GetMetric()
assert.Equal(t, 1, len(m), "should be 1 metric entries")
assert.Equal(t, float64(1), m[0].GetGauge().GetValue())
labels := m[0].GetLabel()
- assert.Equal(t, "subject", labels[0].GetName())
- assert.Equal(t, "beat", labels[0].GetValue())
+ assert.Equal(t, "name", labels[0].GetName())
+ assert.Equal(t, "WS /subscriptions/beat", labels[0].GetValue())
// initiate 1 beat subscription, active websocket should be 2
conn2, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
@@ -137,7 +137,7 @@ func TestWebsocketMetrics(t *testing.T) {
metrics, err = parser.TextToMetricFamilies(bytes.NewReader(body))
assert.Nil(t, err)
- m = metrics["thor_metrics_api_active_websocket_count"].GetMetric()
+ m = metrics["thor_metrics_api_active_websocket_gauge"].GetMetric()
assert.Equal(t, 1, len(m), "should be 1 metric entries")
assert.Equal(t, float64(2), m[0].GetGauge().GetValue())
@@ -151,16 +151,16 @@ func TestWebsocketMetrics(t *testing.T) {
metrics, err = parser.TextToMetricFamilies(bytes.NewReader(body))
assert.Nil(t, err)
- m = metrics["thor_metrics_api_active_websocket_count"].GetMetric()
+ m = metrics["thor_metrics_api_active_websocket_gauge"].GetMetric()
assert.Equal(t, 2, len(m), "should be 2 metric entries")
// both m[0] and m[1] should have the value of 1
assert.Equal(t, float64(2), m[0].GetGauge().GetValue())
assert.Equal(t, float64(1), m[1].GetGauge().GetValue())
- // m[1] should have the subject of block
+ // m[1] should have the name of block
labels = m[1].GetLabel()
- assert.Equal(t, "subject", labels[0].GetName())
- assert.Equal(t, "block", labels[0].GetValue())
+ assert.Equal(t, "name", labels[0].GetName())
+ assert.Equal(t, "WS /subscriptions/block", labels[0].GetValue())
}
func httpGet(t *testing.T, url string) ([]byte, int) {
diff --git a/api/node/node.go b/api/node/node.go
index 11c1ce7e1..21c69dcdf 100644
--- a/api/node/node.go
+++ b/api/node/node.go
@@ -35,6 +35,6 @@ func (n *Node) Mount(root *mux.Router, pathPrefix string) {
sub.Path("/network/peers").
Methods(http.MethodGet).
- Name("node_get_peers").
+ Name("GET /node/network/peers").
HandlerFunc(utils.WrapHandlerFunc(n.handleNetwork))
}
diff --git a/api/transactions/transactions.go b/api/transactions/transactions.go
index af32cb6da..3cf2e4d65 100644
--- a/api/transactions/transactions.go
+++ b/api/transactions/transactions.go
@@ -218,14 +218,14 @@ func (t *Transactions) Mount(root *mux.Router, pathPrefix string) {
sub.Path("").
Methods(http.MethodPost).
- Name("transactions_send_tx").
+ Name("POST /transactions").
HandlerFunc(utils.WrapHandlerFunc(t.handleSendTransaction))
sub.Path("/{id}").
Methods(http.MethodGet).
- Name("transactions_get_tx").
+ Name("GET /transactions/{id}").
HandlerFunc(utils.WrapHandlerFunc(t.handleGetTransactionByID))
sub.Path("/{id}/receipt").
Methods(http.MethodGet).
- Name("transactions_get_receipt").
+ Name("GET /transactions/{id}/receipt").
HandlerFunc(utils.WrapHandlerFunc(t.handleGetTransactionReceiptByID))
}
diff --git a/api/transfers/transfers.go b/api/transfers/transfers.go
index cad4ee6b3..25d2e2599 100644
--- a/api/transfers/transfers.go
+++ b/api/transfers/transfers.go
@@ -90,6 +90,6 @@ func (t *Transfers) Mount(root *mux.Router, pathPrefix string) {
sub.Path("").
Methods(http.MethodPost).
- Name("logs_filter_transfer").
+ Name("POST /logs/transfer").
HandlerFunc(utils.WrapHandlerFunc(t.handleFilterTransferLogs))
}
diff --git a/metrics/telemetry.go b/metrics/telemetry.go
index 9ab3bd633..1d1ee96f2 100644
--- a/metrics/telemetry.go
+++ b/metrics/telemetry.go
@@ -5,7 +5,10 @@
package metrics
-import "net/http"
+import (
+ "net/http"
+ "sync"
+)
// metrics is a singleton service that provides global access to a set of meters
// it wraps multiple implementations and defaults to a no-op implementation
@@ -30,7 +33,11 @@ func HTTPHandler() http.Handler {
// Define standard buckets for histograms
var (
Bucket10s = []int64{0, 500, 1000, 2000, 3000, 4000, 5000, 7500, 10_000}
- BucketHTTPReqs = []int64{0, 150, 300, 450, 600, 900, 1200, 1500, 3000}
+ BucketHTTPReqs = []int64{
+ 0, 1, 2, 5, 10, 20, 30, 50, 75, 100,
+ 150, 200, 300, 400, 500, 750, 1000,
+ 1500, 2000, 3000, 4000, 5000, 10000,
+ }
)
// HistogramMeter represents the type of metric that is calculated by aggregating
@@ -96,12 +103,11 @@ func GaugeVec(name string, labels []string) GaugeVecMeter {
// - it avoid metrics definition to determine the singleton to use (noop vs prometheus)
func LazyLoad[T any](f func() T) func() T {
var result T
- var loaded bool
+ var once sync.Once
return func() T {
- if !loaded {
+ once.Do(func() {
result = f()
- loaded = true
- }
+ })
return result
}
}
From 9eec212d8521e48af3ca5466573e954e24444849 Mon Sep 17 00:00:00 2001
From: Darren Kelly <107671032+darrenvechain@users.noreply.github.com>
Date: Fri, 29 Nov 2024 09:44:09 +0000
Subject: [PATCH 05/15] chore: update builtin generation (#896)
* chore: update builtin generation
* fix: update GHA
---
.github/workflows/lint-go.yaml | 7 +++++++
builtin/gen/bindata.go | 27 +++++++++------------------
builtin/gen/gen.go | 5 +++--
3 files changed, 19 insertions(+), 20 deletions(-)
diff --git a/.github/workflows/lint-go.yaml b/.github/workflows/lint-go.yaml
index ea4d2195a..0f49ceb39 100644
--- a/.github/workflows/lint-go.yaml
+++ b/.github/workflows/lint-go.yaml
@@ -17,6 +17,13 @@ jobs:
with:
go-version: '1.22'
cache: false
+
+ - name: Check `builtins` directory
+ # if it has any changes in the 'builtins' dir after running `go generate`, echo an error and fail the workflow
+ run: |
+ go generate ./builtin/gen
+ git diff --exit-code builtin/gen || (echo "\n\n\nbuiltin/gen directory is not up to date, run 'go generate ./...' to update it" && exit 1)
+
- name: golangci-lint
uses: golangci/golangci-lint-action@v6
with:
diff --git a/builtin/gen/bindata.go b/builtin/gen/bindata.go
index 8f1ffbc5d..c0724a53d 100644
--- a/builtin/gen/bindata.go
+++ b/builtin/gen/bindata.go
@@ -1,4 +1,4 @@
-// Package gen Code generated by go-bindata. (@generated) DO NOT EDIT.
+// Code generated by go-bindata. DO NOT EDIT.
// sources:
// compiled/Authority.abi
// compiled/Authority.bin-runtime
@@ -76,32 +76,21 @@ type bindataFileInfo struct {
modTime time.Time
}
-// Name return file name
func (fi bindataFileInfo) Name() string {
return fi.name
}
-
-// Size return file size
func (fi bindataFileInfo) Size() int64 {
return fi.size
}
-
-// Mode return file mode
func (fi bindataFileInfo) Mode() os.FileMode {
return fi.mode
}
-
-// Mode return file modify time
func (fi bindataFileInfo) ModTime() time.Time {
return fi.modTime
}
-
-// IsDir return file whether a directory
func (fi bindataFileInfo) IsDir() bool {
- return fi.mode&os.ModeDir != 0
+ return false
}
-
-// Sys return file is sys mode
func (fi bindataFileInfo) Sys() interface{} {
return nil
}
@@ -794,11 +783,13 @@ var _bindata = map[string]func() (*asset, error){
// directory embedded in the file by go-bindata.
// For example if you run go-bindata on data/... and data contains the
// following hierarchy:
-// data/
-// foo.txt
-// img/
-// a.png
-// b.png
+//
+// data/
+// foo.txt
+// img/
+// a.png
+// b.png
+//
// then AssetDir("data") would return []string{"foo.txt", "img"}
// AssetDir("data/img") would return []string{"a.png", "b.png"}
// AssetDir("foo.txt") and AssetDir("notexist") would return an error
diff --git a/builtin/gen/gen.go b/builtin/gen/gen.go
index d08c2f179..ce5fed8e1 100644
--- a/builtin/gen/gen.go
+++ b/builtin/gen/gen.go
@@ -6,5 +6,6 @@
package gen
//go:generate rm -rf ./compiled/
-//go:generate solc --optimize-runs 200 --overwrite --bin-runtime --abi -o ./compiled authority.sol energy.sol executor.sol extension.sol extension-v2.sol measure.sol params.sol prototype.sol
-//go:generate go-bindata -nometadata -ignore=_ -pkg gen -o bindata.go compiled/
+//go:generate docker run -v ./:/solidity ethereum/solc:0.4.24 --optimize-runs 200 --overwrite --bin-runtime --abi -o /solidity/compiled authority.sol energy.sol executor.sol extension.sol extension-v2.sol measure.sol params.sol prototype.sol
+//go:generate go run github.com/go-bindata/go-bindata/go-bindata@v1.0.0 -nometadata -ignore=_ -pkg gen -o bindata.go compiled/
+//go:generate go fmt
From f1711c314ea8cdcc2047a382dfb4373c1cdae094 Mon Sep 17 00:00:00 2001
From: Pedro Gomes
Date: Fri, 29 Nov 2024 15:39:20 +0000
Subject: [PATCH 06/15] getreceipts metrics + lint (#902)
---
.../transactions_benchmark_test.go | 536 ++++++++++++++++++
1 file changed, 536 insertions(+)
create mode 100644 api/transactions/transactions_benchmark_test.go
diff --git a/api/transactions/transactions_benchmark_test.go b/api/transactions/transactions_benchmark_test.go
new file mode 100644
index 000000000..f6f1fccd6
--- /dev/null
+++ b/api/transactions/transactions_benchmark_test.go
@@ -0,0 +1,536 @@
+// Copyright (c) 2024 The VeChainThor developers
+
+// Distributed under the GNU Lesser General Public License v3.0 software license, see the accompanying
+// file LICENSE or
+
+package transactions
+
+import (
+ "crypto/ecdsa"
+ "crypto/rand"
+ "fmt"
+ "math"
+ "math/big"
+ "path/filepath"
+ "runtime/debug"
+ "sync"
+ "testing"
+ "time"
+
+ "github.com/elastic/gosigar"
+ "github.com/ethereum/go-ethereum/common/fdlimit"
+ "github.com/ethereum/go-ethereum/crypto"
+ "github.com/pkg/errors"
+ "github.com/stretchr/testify/require"
+ "github.com/vechain/thor/v2/block"
+ "github.com/vechain/thor/v2/chain"
+ "github.com/vechain/thor/v2/cmd/thor/solo"
+ "github.com/vechain/thor/v2/genesis"
+ "github.com/vechain/thor/v2/logdb"
+ "github.com/vechain/thor/v2/muxdb"
+ "github.com/vechain/thor/v2/packer"
+ "github.com/vechain/thor/v2/state"
+ "github.com/vechain/thor/v2/test/datagen"
+ "github.com/vechain/thor/v2/test/testchain"
+ "github.com/vechain/thor/v2/thor"
+ "github.com/vechain/thor/v2/tx"
+ "github.com/vechain/thor/v2/txpool"
+)
+
+var (
+ cachedAccounts []genesis.DevAccount
+ once sync.Once
+ blockCount = 1_000
+)
+
+func getCachedAccounts(b *testing.B) []genesis.DevAccount {
+ once.Do(func() {
+ now := time.Now()
+ cachedAccounts = createAccounts(b, 10_000)
+ b.Logf("Created accounts in: %f secs", time.Since(now).Seconds())
+ })
+ return cachedAccounts
+}
+
+func BenchmarkFetchTx_RealDB_RandomSigners_ManyClausesPerTx(b *testing.B) {
+ // create state accounts
+ accounts := getCachedAccounts(b)
+
+ // randomly pick a signer for signing the transactions
+ randomSignerFunc := randomPickSignerFunc(accounts, createManyClausesPerTx)
+
+ // create test db - will be automagically removed when the benchmark ends
+ db, err := openTempMainDB(b.TempDir())
+ require.NoError(b, err)
+
+ // create blocks
+ newChain, transactions := createPackedChain(b, db, blockCount, accounts, randomSignerFunc)
+
+ // shuffle the transaction into a randomized order
+ randomizedTransactions := shuffleSlice(transactions)
+ b.Logf("About to process %d txs", len(randomizedTransactions))
+
+ // run the benchmarks
+ b.Run("getTransaction", func(b *testing.B) {
+ benchmarkGetTransaction(b, newChain, randomizedTransactions)
+ })
+
+ b.Run("getReceipt", func(b *testing.B) {
+ benchmarkGetReceipt(b, newChain, randomizedTransactions)
+ })
+}
+
+func BenchmarkFetchTx_RealDB_RandomSigners_OneClausePerTx(b *testing.B) {
+ // create state accounts
+ accounts := getCachedAccounts(b)
+
+ // randomly pick a signer for signing the transactions
+ randomSignerFunc := randomPickSignerFunc(accounts, createOneClausePerTx)
+
+ // create test db - will be automagically removed when the benchmark ends
+ db, err := openTempMainDB(b.TempDir())
+ require.NoError(b, err)
+
+ // create blocks
+ newChain, transactions := createPackedChain(b, db, blockCount, accounts, randomSignerFunc)
+
+ // shuffle the transaction into a randomized order
+ randomizedTransactions := shuffleSlice(transactions)
+ b.Logf("About to process %d txs", len(randomizedTransactions))
+
+ // run the benchmarks
+ b.Run("getTransaction", func(b *testing.B) {
+ benchmarkGetTransaction(b, newChain, randomizedTransactions)
+ })
+
+ b.Run("getReceipt", func(b *testing.B) {
+ benchmarkGetReceipt(b, newChain, randomizedTransactions)
+ })
+}
+
+func BenchmarkFetchTx_RandomSigners_ManyClausesPerTx(b *testing.B) {
+ // create state accounts
+ accounts := getCachedAccounts(b)
+
+ // randomly pick a signer for signing the transactions
+ randomSignerFunc := randomPickSignerFunc(accounts, createManyClausesPerTx)
+
+ // create blocks
+ newChain, transactions := createPackedChain(b, muxdb.NewMem(), blockCount, accounts, randomSignerFunc)
+
+ // shuffle the transaction into a randomized order
+ randomizedTransactions := shuffleSlice(transactions)
+ b.Logf("About to process %d txs", len(randomizedTransactions))
+
+ // run the benchmarks
+ b.Run("getTransaction", func(b *testing.B) {
+ benchmarkGetTransaction(b, newChain, randomizedTransactions)
+ })
+
+ b.Run("getReceipt", func(b *testing.B) {
+ benchmarkGetReceipt(b, newChain, randomizedTransactions)
+ })
+}
+
+func BenchmarkFetchTx_RandomSigners_OneClausePerTx(b *testing.B) {
+ // Setup phase: Not part of the benchmark timing
+ b.StopTimer()
+
+ // create state accounts
+ accounts := getCachedAccounts(b)
+
+ // randomly pick a signer for signing the transactions
+ randomSignerFunc := randomPickSignerFunc(accounts, createOneClausePerTx)
+
+ // create blocks
+ newChain, transactions := createPackedChain(b, muxdb.NewMem(), blockCount, accounts, randomSignerFunc)
+
+ // shuffle the transaction into a randomized order
+ randomizedTransactions := shuffleSlice(transactions)
+ b.Logf("About to process %d txs", len(randomizedTransactions))
+
+ // run the benchmarks
+ b.Run("getTransaction", func(b *testing.B) {
+ benchmarkGetTransaction(b, newChain, randomizedTransactions)
+ })
+
+ b.Run("getReceipt", func(b *testing.B) {
+ benchmarkGetReceipt(b, newChain, randomizedTransactions)
+ })
+}
+
+func benchmarkGetTransaction(b *testing.B, thorChain *testchain.Chain, randTxs tx.Transactions) {
+ mempool := txpool.New(thorChain.Repo(), thorChain.Stater(), txpool.Options{Limit: 10, LimitPerAccount: 16, MaxLifetime: 10 * time.Minute})
+ transactionAPI := New(thorChain.Repo(), mempool)
+ head := thorChain.Repo().BestBlockSummary().Header.ID()
+ var err error
+
+ // Measure memory usage
+ b.ReportAllocs()
+
+ // Benchmark execution
+ b.ResetTimer()
+
+ for _, randTx := range randTxs {
+ _, err = transactionAPI.getRawTransaction(randTx.ID(), head, false)
+ if err != nil {
+ b.Fatalf("getRawTransaction failed: %v", err)
+ }
+ }
+}
+
+func benchmarkGetReceipt(b *testing.B, thorChain *testchain.Chain, randTxs tx.Transactions) {
+ mempool := txpool.New(thorChain.Repo(), thorChain.Stater(), txpool.Options{Limit: 10, LimitPerAccount: 16, MaxLifetime: 10 * time.Minute})
+ transactionAPI := New(thorChain.Repo(), mempool)
+ head := thorChain.Repo().BestBlockSummary().Header.ID()
+ var err error
+
+ // Measure memory usage
+ b.ReportAllocs()
+
+ // Benchmark execution
+ b.ResetTimer()
+
+ for _, randTx := range randTxs {
+ _, err = transactionAPI.getTransactionReceiptByID(randTx.ID(), head)
+ if err != nil {
+ b.Fatalf("getTransactionReceiptByID failed: %v", err)
+ }
+ }
+}
+
+func createPackedChain(b *testing.B, db *muxdb.MuxDB, noBlocks int, accounts []genesis.DevAccount, createTxFunc func(chain *testchain.Chain) (tx.Transactions, error)) (*testchain.Chain, tx.Transactions) {
+ proposer := &accounts[0]
+
+ // mock a fake chain for block production
+ fakeChain, err := createChain(db, accounts)
+ require.NoError(b, err)
+
+ // pre-alloc blocks
+ var transactions tx.Transactions
+
+ // Start from the Genesis block
+ previousBlock := fakeChain.GenesisBlock()
+ for i := 0; i < noBlocks; i++ {
+ newTxs, err := createTxFunc(fakeChain)
+ require.NoError(b, err)
+ previousBlock, err = packTxsIntoBlock(
+ fakeChain,
+ proposer,
+ previousBlock,
+ newTxs,
+ )
+ require.NoError(b, err)
+ transactions = append(transactions, newTxs...)
+ }
+
+ return fakeChain, transactions
+}
+
+func createOneClausePerTx(signerPK *ecdsa.PrivateKey, thorChain *testchain.Chain) (tx.Transactions, error) {
+ var transactions tx.Transactions
+ gasUsed := uint64(0)
+ for gasUsed < 9_500_000 {
+ toAddr := datagen.RandAddress()
+ cla := tx.NewClause(&toAddr).WithValue(big.NewInt(10000))
+ transaction := new(tx.Builder).
+ ChainTag(thorChain.Repo().ChainTag()).
+ GasPriceCoef(1).
+ Expiration(math.MaxUint32 - 1).
+ Gas(21_000).
+ Nonce(uint64(datagen.RandInt())).
+ Clause(cla).
+ BlockRef(tx.NewBlockRef(0)).
+ Build()
+
+ sig, err := crypto.Sign(transaction.SigningHash().Bytes(), signerPK)
+ if err != nil {
+ return nil, err
+ }
+ transaction = transaction.WithSignature(sig)
+
+ gasUsed += 21_000 // Gas per transaction
+ transactions = append(transactions, transaction)
+ }
+ return transactions, nil
+}
+
+func createManyClausesPerTx(signerPK *ecdsa.PrivateKey, thorChain *testchain.Chain) (tx.Transactions, error) {
+ var transactions tx.Transactions
+ gasUsed := uint64(0)
+ txGas := uint64(42_000)
+
+ transactionBuilder := new(tx.Builder).
+ ChainTag(thorChain.Repo().ChainTag()).
+ GasPriceCoef(1).
+ Expiration(math.MaxUint32 - 1).
+ Nonce(uint64(datagen.RandInt())).
+ BlockRef(tx.NewBlockRef(0))
+
+ for ; gasUsed < 9_500_000; gasUsed += txGas {
+ toAddr := datagen.RandAddress()
+ transactionBuilder.Clause(tx.NewClause(&toAddr).WithValue(big.NewInt(10000)))
+ }
+
+ transaction := transactionBuilder.Gas(gasUsed).Build()
+
+ sig, err := crypto.Sign(transaction.SigningHash().Bytes(), signerPK)
+ if err != nil {
+ return nil, err
+ }
+ transaction = transaction.WithSignature(sig)
+
+ transactions = append(transactions, transaction)
+
+ return transactions, nil
+}
+
+func packTxsIntoBlock(thorChain *testchain.Chain, proposerAccount *genesis.DevAccount, parentBlk *block.Block, transactions tx.Transactions) (*block.Block, error) {
+ p := packer.New(thorChain.Repo(), thorChain.Stater(), proposerAccount.Address, &proposerAccount.Address, thorChain.GetForkConfig())
+
+ parentSum, err := thorChain.Repo().GetBlockSummary(parentBlk.Header().ID())
+ if err != nil {
+ return nil, err
+ }
+
+ flow, err := p.Schedule(parentSum, parentBlk.Header().Timestamp()+1)
+ if err != nil {
+ return nil, err
+ }
+
+ for _, transaction := range transactions {
+ err = flow.Adopt(transaction)
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ b1, stage, receipts, err := flow.Pack(proposerAccount.PrivateKey, 0, false)
+ if err != nil {
+ return nil, err
+ }
+
+ if _, err := stage.Commit(); err != nil {
+ return nil, err
+ }
+
+ if err := thorChain.Repo().AddBlock(b1, receipts, 0); err != nil {
+ return nil, err
+ }
+
+ if err := thorChain.Repo().SetBestBlockID(b1.Header().ID()); err != nil {
+ return nil, err
+ }
+
+ return b1, nil
+}
+
+func createChain(db *muxdb.MuxDB, accounts []genesis.DevAccount) (*testchain.Chain, error) {
+ forkConfig := thor.NoFork
+ forkConfig.VIP191 = 1
+ forkConfig.BLOCKLIST = 0
+ forkConfig.VIP214 = 2
+
+ // Create the state manager (Stater) with the initialized database.
+ stater := state.NewStater(db)
+
+ authAccs := make([]genesis.Authority, 0, len(accounts))
+ stateAccs := make([]genesis.Account, 0, len(accounts))
+
+ for _, acc := range accounts {
+ authAccs = append(authAccs, genesis.Authority{
+ MasterAddress: acc.Address,
+ EndorsorAddress: acc.Address,
+ Identity: thor.BytesToBytes32([]byte("master")),
+ })
+ bal, _ := new(big.Int).SetString("1000000000000000000000000000", 10)
+ stateAccs = append(stateAccs, genesis.Account{
+ Address: acc.Address,
+ Balance: (*genesis.HexOrDecimal256)(bal),
+ Energy: (*genesis.HexOrDecimal256)(bal),
+ Code: "",
+ Storage: nil,
+ })
+ }
+ mbp := uint64(1_000)
+ genConfig := genesis.CustomGenesis{
+ LaunchTime: 1526400000,
+ GasLimit: thor.InitialGasLimit,
+ ExtraData: "",
+ ForkConfig: &forkConfig,
+ Authority: authAccs,
+ Accounts: stateAccs,
+ Params: genesis.Params{
+ MaxBlockProposers: &mbp,
+ },
+ }
+
+ builder, err := genesis.NewCustomNet(&genConfig)
+ if err != nil {
+ return nil, err
+ }
+
+ // Initialize the genesis and retrieve the genesis block
+ //gene := genesis.NewDevnet()
+ geneBlk, _, _, err := builder.Build(stater)
+ if err != nil {
+ return nil, err
+ }
+
+ // Create the repository which manages chain data, using the database and genesis block.
+ repo, err := chain.NewRepository(db, geneBlk)
+ if err != nil {
+ return nil, err
+ }
+
+ // Create an inMemory logdb
+ logDb, err := logdb.NewMem()
+ if err != nil {
+ return nil, err
+ }
+
+ return testchain.New(
+ db,
+ builder,
+ solo.NewBFTEngine(repo),
+ repo,
+ stater,
+ geneBlk,
+ logDb,
+ thor.NoFork,
+ ), nil
+}
+
+func randomPickSignerFunc(
+ accounts []genesis.DevAccount,
+ createTxFun func(signerPK *ecdsa.PrivateKey, thorChain *testchain.Chain) (tx.Transactions, error),
+) func(chain *testchain.Chain) (tx.Transactions, error) {
+ return func(chain *testchain.Chain) (tx.Transactions, error) {
+ // Ensure there are accounts available
+ if len(accounts) == 0 {
+ return nil, fmt.Errorf("no accounts available to pick a random sender")
+ }
+
+ // Securely pick a random index
+ maxLen := big.NewInt(int64(len(accounts)))
+ randomIndex, err := rand.Int(rand.Reader, maxLen)
+ if err != nil {
+ return nil, fmt.Errorf("failed to generate random index: %v", err)
+ }
+
+ // Use the selected account to create transactions
+ sender := accounts[randomIndex.Int64()]
+ return createTxFun(sender.PrivateKey, chain)
+ }
+}
+
+func createAccounts(b *testing.B, accountNo int) []genesis.DevAccount {
+ var accs []genesis.DevAccount
+
+ for i := 0; i < accountNo; i++ {
+ pk, err := crypto.GenerateKey()
+ require.NoError(b, err)
+ addr := crypto.PubkeyToAddress(pk.PublicKey)
+ accs = append(accs, genesis.DevAccount{Address: thor.Address(addr), PrivateKey: pk})
+ }
+
+ return accs
+}
+
+func openTempMainDB(dir string) (*muxdb.MuxDB, error) {
+ cacheMB := normalizeCacheSize(4096)
+
+ fdCache := suggestFDCache()
+
+ opts := muxdb.Options{
+ TrieNodeCacheSizeMB: cacheMB,
+ TrieRootCacheCapacity: 256,
+ TrieCachedNodeTTL: 30, // 5min
+ TrieLeafBankSlotCapacity: 256,
+ TrieDedupedPartitionFactor: math.MaxUint32,
+ TrieWillCleanHistory: true,
+ OpenFilesCacheCapacity: fdCache,
+ ReadCacheMB: 256, // rely on os page cache other than huge db read cache.
+ WriteBufferMB: 128,
+ }
+
+ // go-ethereum stuff
+ // Ensure Go's GC ignores the database cache for trigger percentage
+ totalCacheMB := cacheMB + opts.ReadCacheMB + opts.WriteBufferMB*2
+ gogc := math.Max(10, math.Min(100, 50/(float64(totalCacheMB)/1024)))
+
+ debug.SetGCPercent(int(gogc))
+
+ if opts.TrieWillCleanHistory {
+ opts.TrieHistPartitionFactor = 256
+ } else {
+ opts.TrieHistPartitionFactor = 524288
+ }
+
+ db, err := muxdb.Open(filepath.Join(dir, "maindb"), &opts)
+ if err != nil {
+ return nil, errors.Wrapf(err, "open main database [%v]", dir)
+ }
+ return db, nil
+}
+
+func normalizeCacheSize(sizeMB int) int {
+ if sizeMB < 128 {
+ sizeMB = 128
+ }
+
+ var mem gosigar.Mem
+ if err := mem.Get(); err != nil {
+ fmt.Println("failed to get total mem:", "err", err)
+ } else {
+ total := int(mem.Total / 1024 / 1024)
+ half := total / 2
+
+ // limit to not less than total/2 and up to total-2GB
+ limitMB := total - 2048
+ if limitMB < half {
+ limitMB = half
+ }
+
+ if sizeMB > limitMB {
+ sizeMB = limitMB
+ fmt.Println("cache size(MB) limited", "limit", limitMB)
+ }
+ }
+ return sizeMB
+}
+
+func suggestFDCache() int {
+ limit, err := fdlimit.Current()
+ if err != nil {
+ fmt.Println("unable to get fdlimit", "error", err)
+ return 500
+ }
+ if limit <= 1024 {
+ fmt.Println("low fd limit, increase it if possible", "limit", limit)
+ }
+
+ n := limit / 2
+ if n > 5120 {
+ return 5120
+ }
+ return n
+}
+
+func shuffleSlice(slice tx.Transactions) tx.Transactions {
+ shuffled := make(tx.Transactions, len(slice))
+ copy(shuffled, slice)
+
+ for i := len(shuffled) - 1; i > 0; i-- {
+ n, err := rand.Int(rand.Reader, big.NewInt(int64(i+1)))
+ if err != nil {
+ panic(err) // Handle errors appropriately in real code
+ }
+
+ // Swap the current element with the random index
+ j := int(n.Int64())
+ shuffled[i], shuffled[j] = shuffled[j], shuffled[i]
+ }
+
+ return shuffled
+}
From a1576965cf5cad71a78ff6d9b3ce6b389681ff5e Mon Sep 17 00:00:00 2001
From: Darren Kelly <107671032+darrenvechain@users.noreply.github.com>
Date: Mon, 2 Dec 2024 16:01:35 +0000
Subject: [PATCH 07/15] chore: add flag to enable/disable deprecated APIs
(#897)
* chore: add flag to enable/disable deprecated APIs
* chore: update for PR comments
* chore: update for PR comments
* fix: update e2e commit sha
* fix: update e2e commit sha
* fix: update flag name
---
.github/workflows/test-e2e.yaml | 4 +--
api/accounts/accounts.go | 16 ++++++---
api/accounts/accounts_test.go | 21 +++++++++--
api/api.go | 47 ++++++++++++++-----------
api/metrics_test.go | 4 +--
api/subscriptions/subscriptions.go | 27 ++++++++------
api/subscriptions/subscriptions_test.go | 19 +++++++---
cmd/thor/flags.go | 4 +++
cmd/thor/main.go | 28 +++------------
cmd/thor/utils.go | 18 ++++++++++
thorclient/api_test.go | 2 +-
11 files changed, 118 insertions(+), 72 deletions(-)
diff --git a/.github/workflows/test-e2e.yaml b/.github/workflows/test-e2e.yaml
index babbb3a39..55c82689e 100644
--- a/.github/workflows/test-e2e.yaml
+++ b/.github/workflows/test-e2e.yaml
@@ -43,8 +43,8 @@ jobs:
uses: actions/checkout@v4
with:
repository: vechain/thor-e2e-tests
- # https://github.com/vechain/thor-e2e-tests/tree/209f6ea9a81a98dc2d5e42bf036d2878c5837036
- ref: 209f6ea9a81a98dc2d5e42bf036d2878c5837036
+ # https://github.com/vechain/thor-e2e-tests/tree/8b72bedff11c9e8873d88b6e2dba356d43b56779
+ ref: 8b72bedff11c9e8873d88b6e2dba356d43b56779
- name: Download artifact
uses: actions/download-artifact@v4
diff --git a/api/accounts/accounts.go b/api/accounts/accounts.go
index 54058a160..22698bdbd 100644
--- a/api/accounts/accounts.go
+++ b/api/accounts/accounts.go
@@ -27,11 +27,12 @@ import (
)
type Accounts struct {
- repo *chain.Repository
- stater *state.Stater
- callGasLimit uint64
- forkConfig thor.ForkConfig
- bft bft.Committer
+ repo *chain.Repository
+ stater *state.Stater
+ callGasLimit uint64
+ forkConfig thor.ForkConfig
+ bft bft.Committer
+ enabledDeprecated bool
}
func New(
@@ -40,6 +41,7 @@ func New(
callGasLimit uint64,
forkConfig thor.ForkConfig,
bft bft.Committer,
+ enabledDeprecated bool,
) *Accounts {
return &Accounts{
repo,
@@ -47,6 +49,7 @@ func New(
callGasLimit,
forkConfig,
bft,
+ enabledDeprecated,
}
}
@@ -168,6 +171,9 @@ func (a *Accounts) handleGetStorage(w http.ResponseWriter, req *http.Request) er
}
func (a *Accounts) handleCallContract(w http.ResponseWriter, req *http.Request) error {
+ if !a.enabledDeprecated {
+ return utils.HTTPError(nil, http.StatusGone)
+ }
callData := &CallData{}
if err := utils.ParseJSON(req.Body, &callData); err != nil {
return utils.BadRequest(errors.WithMessage(err, "body"))
diff --git a/api/accounts/accounts_test.go b/api/accounts/accounts_test.go
index 9294723eb..8630bea4b 100644
--- a/api/accounts/accounts_test.go
+++ b/api/accounts/accounts_test.go
@@ -103,7 +103,7 @@ var (
)
func TestAccount(t *testing.T) {
- initAccountServer(t)
+ initAccountServer(t, true)
defer ts.Close()
tclient = thorclient.New(ts.URL)
@@ -126,6 +126,21 @@ func TestAccount(t *testing.T) {
}
}
+func TestDeprecated(t *testing.T) {
+ initAccountServer(t, false)
+ defer ts.Close()
+
+ tclient = thorclient.New(ts.URL)
+
+ body := &accounts.CallData{}
+
+ _, statusCode, _ := tclient.RawHTTPClient().RawHTTPPost("/accounts", body)
+ assert.Equal(t, http.StatusGone, statusCode, "invalid address")
+
+ _, statusCode, _ = tclient.RawHTTPClient().RawHTTPPost("/accounts/"+contractAddr.String(), body)
+ assert.Equal(t, http.StatusGone, statusCode, "invalid address")
+}
+
func getAccount(t *testing.T) {
_, statusCode, err := tclient.RawHTTPClient().RawHTTPGet("/accounts/" + invalidAddr)
require.NoError(t, err)
@@ -264,7 +279,7 @@ func getStorageWithNonExistingRevision(t *testing.T) {
assert.Equal(t, "revision: leveldb: not found\n", string(res), "revision not found")
}
-func initAccountServer(t *testing.T) {
+func initAccountServer(t *testing.T, enabledDeprecated bool) {
thorChain, err := testchain.NewIntegrationTestChain()
require.NoError(t, err)
@@ -291,7 +306,7 @@ func initAccountServer(t *testing.T) {
)
router := mux.NewRouter()
- accounts.New(thorChain.Repo(), thorChain.Stater(), uint64(gasLimit), thor.NoFork, thorChain.Engine()).
+ accounts.New(thorChain.Repo(), thorChain.Stater(), uint64(gasLimit), thor.NoFork, thorChain.Engine(), enabledDeprecated).
Mount(router, "/accounts")
ts = httptest.NewServer(router)
diff --git a/api/api.go b/api/api.go
index 38b412a97..0385929ec 100644
--- a/api/api.go
+++ b/api/api.go
@@ -32,6 +32,21 @@ import (
var logger = log.WithContext("pkg", "api")
+type Config struct {
+ AllowedOrigins string
+ BacktraceLimit uint32
+ CallGasLimit uint64
+ PprofOn bool
+ SkipLogs bool
+ AllowCustomTracer bool
+ EnableReqLogger bool
+ EnableMetrics bool
+ LogsLimit uint64
+ AllowedTracers []string
+ SoloMode bool
+ EnableDeprecated bool
+}
+
// New return api router
func New(
repo *chain.Repository,
@@ -41,19 +56,9 @@ func New(
bft bft.Committer,
nw node.Network,
forkConfig thor.ForkConfig,
- allowedOrigins string,
- backtraceLimit uint32,
- callGasLimit uint64,
- pprofOn bool,
- skipLogs bool,
- allowCustomTracer bool,
- enableReqLogger bool,
- enableMetrics bool,
- logsLimit uint64,
- allowedTracers []string,
- soloMode bool,
+ config Config,
) (http.HandlerFunc, func()) {
- origins := strings.Split(strings.TrimSpace(allowedOrigins), ",")
+ origins := strings.Split(strings.TrimSpace(config.AllowedOrigins), ",")
for i, o := range origins {
origins[i] = strings.ToLower(strings.TrimSpace(o))
}
@@ -71,27 +76,27 @@ func New(
http.Redirect(w, req, "doc/stoplight-ui/", http.StatusTemporaryRedirect)
})
- accounts.New(repo, stater, callGasLimit, forkConfig, bft).
+ accounts.New(repo, stater, config.CallGasLimit, forkConfig, bft, config.EnableDeprecated).
Mount(router, "/accounts")
- if !skipLogs {
- events.New(repo, logDB, logsLimit).
+ if !config.SkipLogs {
+ events.New(repo, logDB, config.LogsLimit).
Mount(router, "/logs/event")
- transfers.New(repo, logDB, logsLimit).
+ transfers.New(repo, logDB, config.LogsLimit).
Mount(router, "/logs/transfer")
}
blocks.New(repo, bft).
Mount(router, "/blocks")
transactions.New(repo, txPool).
Mount(router, "/transactions")
- debug.New(repo, stater, forkConfig, callGasLimit, allowCustomTracer, bft, allowedTracers, soloMode).
+ debug.New(repo, stater, forkConfig, config.CallGasLimit, config.AllowCustomTracer, bft, config.AllowedTracers, config.SoloMode).
Mount(router, "/debug")
node.New(nw).
Mount(router, "/node")
- subs := subscriptions.New(repo, origins, backtraceLimit, txPool)
+ subs := subscriptions.New(repo, origins, config.BacktraceLimit, txPool, config.EnableDeprecated)
subs.Mount(router, "/subscriptions")
- if pprofOn {
+ if config.PprofOn {
router.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
router.HandleFunc("/debug/pprof/profile", pprof.Profile)
router.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
@@ -99,7 +104,7 @@ func New(
router.PathPrefix("/debug/pprof/").HandlerFunc(pprof.Index)
}
- if enableMetrics {
+ if config.EnableMetrics {
router.Use(metricsMiddleware)
}
@@ -110,7 +115,7 @@ func New(
handlers.ExposedHeaders([]string{"x-genesis-id", "x-thorest-ver"}),
)(handler)
- if enableReqLogger {
+ if config.EnableReqLogger {
handler = RequestLoggerHandler(handler, logger)
}
diff --git a/api/metrics_test.go b/api/metrics_test.go
index 4e4b6daaf..9b83a08a6 100644
--- a/api/metrics_test.go
+++ b/api/metrics_test.go
@@ -48,7 +48,7 @@ func TestMetricsMiddleware(t *testing.T) {
assert.NotNil(t, err)
router := mux.NewRouter()
- acc := accounts.New(thorChain.Repo(), thorChain.Stater(), math.MaxUint64, thor.NoFork, thorChain.Engine())
+ acc := accounts.New(thorChain.Repo(), thorChain.Stater(), math.MaxUint64, thor.NoFork, thorChain.Engine(), true)
acc.Mount(router, "/accounts")
router.PathPrefix("/metrics").Handler(metrics.HTTPHandler())
router.Use(metricsMiddleware)
@@ -103,7 +103,7 @@ func TestWebsocketMetrics(t *testing.T) {
require.NoError(t, err)
router := mux.NewRouter()
- sub := subscriptions.New(thorChain.Repo(), []string{"*"}, 10, txpool.New(thorChain.Repo(), thorChain.Stater(), txpool.Options{}))
+ sub := subscriptions.New(thorChain.Repo(), []string{"*"}, 10, txpool.New(thorChain.Repo(), thorChain.Stater(), txpool.Options{}), true)
sub.Mount(router, "/subscriptions")
router.PathPrefix("/metrics").Handler(metrics.HTTPHandler())
router.Use(metricsMiddleware)
diff --git a/api/subscriptions/subscriptions.go b/api/subscriptions/subscriptions.go
index 7582da5bb..715a71308 100644
--- a/api/subscriptions/subscriptions.go
+++ b/api/subscriptions/subscriptions.go
@@ -25,14 +25,15 @@ import (
const txQueueSize = 20
type Subscriptions struct {
- backtraceLimit uint32
- repo *chain.Repository
- upgrader *websocket.Upgrader
- pendingTx *pendingTx
- done chan struct{}
- wg sync.WaitGroup
- beat2Cache *messageCache[Beat2Message]
- beatCache *messageCache[BeatMessage]
+ backtraceLimit uint32
+ enabledDeprecated bool
+ repo *chain.Repository
+ upgrader *websocket.Upgrader
+ pendingTx *pendingTx
+ done chan struct{}
+ wg sync.WaitGroup
+ beat2Cache *messageCache[Beat2Message]
+ beatCache *messageCache[BeatMessage]
}
type msgReader interface {
@@ -50,10 +51,11 @@ const (
pingPeriod = (pongWait * 7) / 10
)
-func New(repo *chain.Repository, allowedOrigins []string, backtraceLimit uint32, txpool *txpool.TxPool) *Subscriptions {
+func New(repo *chain.Repository, allowedOrigins []string, backtraceLimit uint32, txpool *txpool.TxPool, enabledDeprecated bool) *Subscriptions {
sub := &Subscriptions{
- backtraceLimit: backtraceLimit,
- repo: repo,
+ backtraceLimit: backtraceLimit,
+ repo: repo,
+ enabledDeprecated: enabledDeprecated,
upgrader: &websocket.Upgrader{
EnableCompression: true,
CheckOrigin: func(r *http.Request) bool {
@@ -195,6 +197,9 @@ func (s *Subscriptions) handleSubject(w http.ResponseWriter, req *http.Request)
return err
}
case "beat":
+ if !s.enabledDeprecated {
+ return utils.HTTPError(nil, http.StatusGone)
+ }
if reader, err = s.handleBeatReader(w, req); err != nil {
return err
}
diff --git a/api/subscriptions/subscriptions_test.go b/api/subscriptions/subscriptions_test.go
index 0c0bffe3a..8cfb55f7f 100644
--- a/api/subscriptions/subscriptions_test.go
+++ b/api/subscriptions/subscriptions_test.go
@@ -36,7 +36,7 @@ var ts *httptest.Server
var blocks []*block.Block
func TestSubscriptions(t *testing.T) {
- initSubscriptionsServer(t)
+ initSubscriptionsServer(t, true)
defer ts.Close()
for name, tt := range map[string]func(*testing.T){
@@ -51,6 +51,17 @@ func TestSubscriptions(t *testing.T) {
}
}
+func TestDeprecatedSubscriptions(t *testing.T) {
+ initSubscriptionsServer(t, false)
+ defer ts.Close()
+
+ u := url.URL{Scheme: "ws", Host: strings.TrimPrefix(ts.URL, "http://"), Path: "/subscriptions/beat"}
+
+ _, resp, err := websocket.DefaultDialer.Dial(u.String(), nil)
+ assert.Error(t, err)
+ assert.Equal(t, http.StatusGone, resp.StatusCode)
+}
+
func testHandleSubjectWithBlock(t *testing.T) {
genesisBlock := blocks[0]
queryArg := fmt.Sprintf("pos=%s", genesisBlock.Header().ID().String())
@@ -216,7 +227,7 @@ func TestParseAddress(t *testing.T) {
assert.Equal(t, expectedAddr, *result)
}
-func initSubscriptionsServer(t *testing.T) {
+func initSubscriptionsServer(t *testing.T, enabledDeprecated bool) {
thorChain, err := testchain.NewIntegrationTestChain()
require.NoError(t, err)
@@ -263,7 +274,7 @@ func initSubscriptionsServer(t *testing.T) {
require.NoError(t, err)
router := mux.NewRouter()
- New(thorChain.Repo(), []string{}, 5, txPool).
+ New(thorChain.Repo(), []string{}, 5, txPool, enabledDeprecated).
Mount(router, "/subscriptions")
ts = httptest.NewServer(router)
}
@@ -319,7 +330,7 @@ func TestSubscriptionsBacktrace(t *testing.T) {
require.NoError(t, err)
router := mux.NewRouter()
- New(thorChain.Repo(), []string{}, 5, txPool).Mount(router, "/subscriptions")
+ New(thorChain.Repo(), []string{}, 5, txPool, true).Mount(router, "/subscriptions")
ts = httptest.NewServer(router)
defer ts.Close()
diff --git a/cmd/thor/flags.go b/cmd/thor/flags.go
index 4b18f22ad..2ce97516e 100644
--- a/cmd/thor/flags.go
+++ b/cmd/thor/flags.go
@@ -69,6 +69,10 @@ var (
Value: 1000,
Usage: "limit the number of logs returned by /logs API",
}
+ apiEnableDeprecatedFlag = cli.BoolFlag{
+ Name: "api-enable-deprecated",
+ Usage: "enable deprecated API endpoints (POST /accounts/{address}, POST /accounts, WS /subscriptions/beat",
+ }
enableAPILogsFlag = cli.BoolFlag{
Name: "enable-api-logs",
Usage: "enables API requests logging",
diff --git a/cmd/thor/main.go b/cmd/thor/main.go
index 4b934bc13..7ddccc08c 100644
--- a/cmd/thor/main.go
+++ b/cmd/thor/main.go
@@ -11,7 +11,6 @@ import (
"io"
"os"
"path/filepath"
- "strings"
"time"
"github.com/ethereum/go-ethereum/accounts/keystore"
@@ -80,6 +79,7 @@ func main() {
apiCallGasLimitFlag,
apiBacktraceLimitFlag,
apiAllowCustomTracerFlag,
+ apiEnableDeprecatedFlag,
enableAPILogsFlag,
apiLogsLimitFlag,
verbosityFlag,
@@ -115,6 +115,7 @@ func main() {
apiCallGasLimitFlag,
apiBacktraceLimitFlag,
apiAllowCustomTracerFlag,
+ apiEnableDeprecatedFlag,
enableAPILogsFlag,
apiLogsLimitFlag,
onDemandFlag,
@@ -255,17 +256,7 @@ func defaultAction(ctx *cli.Context) error {
bftEngine,
p2pCommunicator.Communicator(),
forkConfig,
- ctx.String(apiCorsFlag.Name),
- uint32(ctx.Uint64(apiBacktraceLimitFlag.Name)),
- ctx.Uint64(apiCallGasLimitFlag.Name),
- ctx.Bool(pprofFlag.Name),
- skipLogs,
- ctx.Bool(apiAllowCustomTracerFlag.Name),
- ctx.Bool(enableAPILogsFlag.Name),
- ctx.Bool(enableMetricsFlag.Name),
- ctx.Uint64(apiLogsLimitFlag.Name),
- parseTracerList(strings.TrimSpace(ctx.String(allowedTracersFlag.Name))),
- false,
+ makeAPIConfig(ctx, false),
)
defer func() { log.Info("closing API..."); apiCloser() }()
@@ -399,6 +390,7 @@ func soloAction(ctx *cli.Context) error {
defer func() { log.Info("closing tx pool..."); txPool.Close() }()
bftEngine := solo.NewBFTEngine(repo)
+
apiHandler, apiCloser := api.New(
repo,
state.NewStater(mainDB),
@@ -407,17 +399,7 @@ func soloAction(ctx *cli.Context) error {
bftEngine,
&solo.Communicator{},
forkConfig,
- ctx.String(apiCorsFlag.Name),
- uint32(ctx.Uint64(apiBacktraceLimitFlag.Name)),
- ctx.Uint64(apiCallGasLimitFlag.Name),
- ctx.Bool(pprofFlag.Name),
- skipLogs,
- ctx.Bool(apiAllowCustomTracerFlag.Name),
- ctx.Bool(enableAPILogsFlag.Name),
- ctx.Bool(enableMetricsFlag.Name),
- ctx.Uint64(apiLogsLimitFlag.Name),
- parseTracerList(strings.TrimSpace(ctx.String(allowedTracersFlag.Name))),
- true,
+ makeAPIConfig(ctx, false),
)
defer func() { log.Info("closing API..."); apiCloser() }()
diff --git a/cmd/thor/utils.go b/cmd/thor/utils.go
index 5c6799354..6877a45ee 100644
--- a/cmd/thor/utils.go
+++ b/cmd/thor/utils.go
@@ -37,6 +37,7 @@ import (
"github.com/mattn/go-isatty"
"github.com/mattn/go-tty"
"github.com/pkg/errors"
+ "github.com/vechain/thor/v2/api"
"github.com/vechain/thor/v2/api/doc"
"github.com/vechain/thor/v2/chain"
"github.com/vechain/thor/v2/cmd/thor/node"
@@ -274,6 +275,23 @@ func parseGenesisFile(filePath string) (*genesis.Genesis, thor.ForkConfig, error
return customGen, forkConfig, nil
}
+func makeAPIConfig(ctx *cli.Context, soloMode bool) api.Config {
+ return api.Config{
+ AllowedOrigins: ctx.String(apiCorsFlag.Name),
+ BacktraceLimit: uint32(ctx.Uint64(apiBacktraceLimitFlag.Name)),
+ CallGasLimit: ctx.Uint64(apiCallGasLimitFlag.Name),
+ PprofOn: ctx.Bool(pprofFlag.Name),
+ SkipLogs: ctx.Bool(skipLogsFlag.Name),
+ AllowCustomTracer: ctx.Bool(apiAllowCustomTracerFlag.Name),
+ EnableReqLogger: ctx.Bool(enableAPILogsFlag.Name),
+ EnableMetrics: ctx.Bool(enableMetricsFlag.Name),
+ LogsLimit: ctx.Uint64(apiLogsLimitFlag.Name),
+ AllowedTracers: parseTracerList(strings.TrimSpace(ctx.String(allowedTracersFlag.Name))),
+ EnableDeprecated: ctx.Bool(apiEnableDeprecatedFlag.Name),
+ SoloMode: soloMode,
+ }
+}
+
func makeConfigDir(ctx *cli.Context) (string, error) {
dir := ctx.String(configDirFlag.Name)
if dir == "" {
diff --git a/thorclient/api_test.go b/thorclient/api_test.go
index e6a0e43be..e8ae49a8a 100644
--- a/thorclient/api_test.go
+++ b/thorclient/api_test.go
@@ -50,7 +50,7 @@ func initAPIServer(t *testing.T) (*testchain.Chain, *httptest.Server) {
router := mux.NewRouter()
- accounts.New(thorChain.Repo(), thorChain.Stater(), uint64(gasLimit), thor.NoFork, thorChain.Engine()).
+ accounts.New(thorChain.Repo(), thorChain.Stater(), uint64(gasLimit), thor.NoFork, thorChain.Engine(), true).
Mount(router, "/accounts")
blocks.New(thorChain.Repo(), thorChain.Engine()).Mount(router, "/blocks")
From cebfc39f8bc9e8b97e1e20d79ab1cdca4e1551ff Mon Sep 17 00:00:00 2001
From: Darren Kelly <107671032+darrenvechain@users.noreply.github.com>
Date: Wed, 4 Dec 2024 16:44:52 +0000
Subject: [PATCH 08/15] fix: solo start flags (#906)
---
cmd/thor/main.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/cmd/thor/main.go b/cmd/thor/main.go
index 7ddccc08c..15531e091 100644
--- a/cmd/thor/main.go
+++ b/cmd/thor/main.go
@@ -399,7 +399,7 @@ func soloAction(ctx *cli.Context) error {
bftEngine,
&solo.Communicator{},
forkConfig,
- makeAPIConfig(ctx, false),
+ makeAPIConfig(ctx, true),
)
defer func() { log.Info("closing API..."); apiCloser() }()
From 1ea83b2cf3a26a17c865ea38023cfb25a2e52058 Mon Sep 17 00:00:00 2001
From: Darren Kelly <107671032+darrenvechain@users.noreply.github.com>
Date: Fri, 6 Dec 2024 10:22:27 +0000
Subject: [PATCH 09/15] chore: make thorclient configurable + fix type error
(#908)
* chore: make thorclient configurable
* fix: subscriptions block type
* fix: compile errors
* fix: remove test with lint error
---
thorclient/httpclient/client.go | 6 +++++-
thorclient/thorclient.go | 10 +++++++++-
thorclient/wsclient/client.go | 5 ++---
thorclient/wsclient/client_test.go | 14 ++++++--------
4 files changed, 22 insertions(+), 13 deletions(-)
diff --git a/thorclient/httpclient/client.go b/thorclient/httpclient/client.go
index 8f88783f5..ce05bf17f 100644
--- a/thorclient/httpclient/client.go
+++ b/thorclient/httpclient/client.go
@@ -33,9 +33,13 @@ type Client struct {
// New creates a new Client with the provided URL.
func New(url string) *Client {
+ return NewWithHTTP(url, http.DefaultClient)
+}
+
+func NewWithHTTP(url string, c *http.Client) *Client {
return &Client{
url: url,
- c: &http.Client{},
+ c: c,
}
}
diff --git a/thorclient/thorclient.go b/thorclient/thorclient.go
index 8458a0ae4..0b7939f51 100644
--- a/thorclient/thorclient.go
+++ b/thorclient/thorclient.go
@@ -11,6 +11,7 @@ package thorclient
import (
"fmt"
+ "net/http"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/common/math"
@@ -44,6 +45,13 @@ func New(url string) *Client {
}
}
+// NewWithHTTP creates a new Client using the provided HTTP URL and HTTP client.
+func NewWithHTTP(url string, c *http.Client) *Client {
+ return &Client{
+ httpConn: httpclient.NewWithHTTP(url, c),
+ }
+}
+
// NewWithWS creates a new Client using the provided HTTP and WebSocket URLs.
// Returns an error if the WebSocket connection fails.
func NewWithWS(url string) (*Client, error) {
@@ -202,7 +210,7 @@ func (c *Client) ChainTag() (byte, error) {
}
// SubscribeBlocks subscribes to block updates over WebSocket.
-func (c *Client) SubscribeBlocks(pos string) (*common.Subscription[*blocks.JSONCollapsedBlock], error) {
+func (c *Client) SubscribeBlocks(pos string) (*common.Subscription[*subscriptions.BlockMessage], error) {
if c.wsConn == nil {
return nil, fmt.Errorf("not a websocket typed client")
}
diff --git a/thorclient/wsclient/client.go b/thorclient/wsclient/client.go
index 057d5aa48..9eb1519ab 100644
--- a/thorclient/wsclient/client.go
+++ b/thorclient/wsclient/client.go
@@ -16,7 +16,6 @@ import (
"github.com/vechain/thor/v2/thor"
"github.com/gorilla/websocket"
- "github.com/vechain/thor/v2/api/blocks"
"github.com/vechain/thor/v2/api/subscriptions"
"github.com/vechain/thor/v2/thorclient/common"
)
@@ -89,7 +88,7 @@ func (c *Client) SubscribeEvents(pos string, filter *subscriptions.EventFilter)
// SubscribeBlocks subscribes to block updates based on the provided query.
// It returns a Subscription that streams block messages or an error if the connection fails.
-func (c *Client) SubscribeBlocks(pos string) (*common.Subscription[*blocks.JSONCollapsedBlock], error) {
+func (c *Client) SubscribeBlocks(pos string) (*common.Subscription[*subscriptions.BlockMessage], error) {
queryValues := &url.Values{}
queryValues.Add("pos", pos)
conn, err := c.connect("/subscriptions/block", queryValues)
@@ -97,7 +96,7 @@ func (c *Client) SubscribeBlocks(pos string) (*common.Subscription[*blocks.JSONC
return nil, fmt.Errorf("unable to connect - %w", err)
}
- return subscribe[blocks.JSONCollapsedBlock](conn), nil
+ return subscribe[subscriptions.BlockMessage](conn), nil
}
// SubscribeTransfers subscribes to transfer events based on the provided query.
diff --git a/thorclient/wsclient/client_test.go b/thorclient/wsclient/client_test.go
index 483ae7233..19dd1b395 100644
--- a/thorclient/wsclient/client_test.go
+++ b/thorclient/wsclient/client_test.go
@@ -13,13 +13,11 @@ import (
"testing"
"time"
- "github.com/vechain/thor/v2/test/datagen"
- "github.com/vechain/thor/v2/thor"
-
"github.com/gorilla/websocket"
"github.com/stretchr/testify/assert"
- "github.com/vechain/thor/v2/api/blocks"
"github.com/vechain/thor/v2/api/subscriptions"
+ "github.com/vechain/thor/v2/test/datagen"
+ "github.com/vechain/thor/v2/thor"
"github.com/vechain/thor/v2/thorclient/common"
)
@@ -50,7 +48,7 @@ func TestClient_SubscribeEvents(t *testing.T) {
func TestClient_SubscribeBlocks(t *testing.T) {
pos := "best"
- expectedBlock := &blocks.JSONCollapsedBlock{}
+ expectedBlock := &subscriptions.BlockMessage{}
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/subscriptions/block", r.URL.Path)
@@ -288,7 +286,7 @@ func TestClient_SubscribeBlocks_ServerError(t *testing.T) {
func TestClient_SubscribeBlocks_ServerShutdown(t *testing.T) {
pos := "best"
- expectedBlock := &blocks.JSONCollapsedBlock{}
+ expectedBlock := &subscriptions.BlockMessage{}
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/subscriptions/block", r.URL.Path)
@@ -325,7 +323,7 @@ func TestClient_SubscribeBlocks_ServerShutdown(t *testing.T) {
func TestClient_SubscribeBlocks_ClientShutdown(t *testing.T) {
pos := "best"
- expectedBlock := &blocks.JSONCollapsedBlock{}
+ expectedBlock := &subscriptions.BlockMessage{}
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/subscriptions/block", r.URL.Path)
@@ -377,7 +375,7 @@ func TestClient_SubscribeBlocks_ClientShutdown(t *testing.T) {
func TestClient_SubscribeBlocks_ClientShutdown_LongBlocks(t *testing.T) {
pos := "best"
- expectedBlock := &blocks.JSONCollapsedBlock{}
+ expectedBlock := &subscriptions.BlockMessage{}
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/subscriptions/block", r.URL.Path)
From 3aa2935248ee709f3e0230e2679613c14c0f837a Mon Sep 17 00:00:00 2001
From: YeahNotSewerSide <47860375+YeahNotSewerSide@users.noreply.github.com>
Date: Fri, 6 Dec 2024 12:31:28 +0200
Subject: [PATCH 10/15] add 'raw' query parameter to the blocks (#899)
* add 'raw' query parameter to the blocks
* summary -> summary.Header
Co-authored-by: libotony
* change variable name
* make expanded and raw mutually exclusive
* add unit tests
* fix linting
---------
Co-authored-by: libotony
---
api/blocks/blocks.go | 29 ++++++++++++++++++---
api/blocks/blocks_test.go | 55 +++++++++++++++++++++++++++++++++++++++
api/blocks/types.go | 4 +++
api/doc/thor.yaml | 13 +++++++++
api/utils/http.go | 13 +++++++++
5 files changed, 110 insertions(+), 4 deletions(-)
diff --git a/api/blocks/blocks.go b/api/blocks/blocks.go
index ff86e02e6..a8e072a02 100644
--- a/api/blocks/blocks.go
+++ b/api/blocks/blocks.go
@@ -6,8 +6,11 @@
package blocks
import (
+ "encoding/hex"
+ "fmt"
"net/http"
+ "github.com/ethereum/go-ethereum/rlp"
"github.com/gorilla/mux"
"github.com/pkg/errors"
"github.com/vechain/thor/v2/api/utils"
@@ -34,9 +37,17 @@ func (b *Blocks) handleGetBlock(w http.ResponseWriter, req *http.Request) error
if err != nil {
return utils.BadRequest(errors.WithMessage(err, "revision"))
}
- expanded := req.URL.Query().Get("expanded")
- if expanded != "" && expanded != "false" && expanded != "true" {
- return utils.BadRequest(errors.WithMessage(errors.New("should be boolean"), "expanded"))
+ raw, err := utils.StringToBoolean(req.URL.Query().Get("raw"), false)
+ if err != nil {
+ return utils.BadRequest(errors.WithMessage(err, "raw"))
+ }
+ expanded, err := utils.StringToBoolean(req.URL.Query().Get("expanded"), false)
+ if err != nil {
+ return utils.BadRequest(errors.WithMessage(err, "expanded"))
+ }
+
+ if raw && expanded {
+ return utils.BadRequest(errors.WithMessage(errors.New("Raw and Expanded are mutually exclusive"), "raw&expanded"))
}
summary, err := utils.GetSummary(revision, b.repo, b.bft)
@@ -47,6 +58,16 @@ func (b *Blocks) handleGetBlock(w http.ResponseWriter, req *http.Request) error
return err
}
+ if raw {
+ rlpEncoded, err := rlp.EncodeToBytes(summary.Header)
+ if err != nil {
+ return err
+ }
+ return utils.WriteJSON(w, &JSONRawBlockSummary{
+ fmt.Sprintf("0x%s", hex.EncodeToString(rlpEncoded)),
+ })
+ }
+
isTrunk, err := b.isTrunk(summary.Header.ID(), summary.Header.Number())
if err != nil {
return err
@@ -61,7 +82,7 @@ func (b *Blocks) handleGetBlock(w http.ResponseWriter, req *http.Request) error
}
jSummary := buildJSONBlockSummary(summary, isTrunk, isFinalized)
- if expanded == "true" {
+ if expanded {
txs, err := b.repo.GetBlockTransactions(summary.Header.ID())
if err != nil {
return err
diff --git a/api/blocks/blocks_test.go b/api/blocks/blocks_test.go
index dcb6c4e94..8c0439e59 100644
--- a/api/blocks/blocks_test.go
+++ b/api/blocks/blocks_test.go
@@ -6,6 +6,7 @@
package blocks_test
import (
+ "encoding/hex"
"encoding/json"
"math"
"math/big"
@@ -15,6 +16,7 @@ import (
"strings"
"testing"
+ "github.com/ethereum/go-ethereum/rlp"
"github.com/gorilla/mux"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -55,6 +57,8 @@ func TestBlock(t *testing.T) {
"testGetFinalizedBlock": testGetFinalizedBlock,
"testGetJustifiedBlock": testGetJustifiedBlock,
"testGetBlockWithRevisionNumberTooHigh": testGetBlockWithRevisionNumberTooHigh,
+ "testMutuallyExclusiveQueries": testMutuallyExclusiveQueries,
+ "testGetRawBlock": testGetRawBlock,
} {
t.Run(name, tt)
}
@@ -67,6 +71,22 @@ func testBadQueryParams(t *testing.T) {
assert.Equal(t, http.StatusBadRequest, statusCode)
assert.Equal(t, "expanded: should be boolean", strings.TrimSpace(string(res)))
+
+ badQueryParams = "?raw=1"
+ res, statusCode, err = tclient.RawHTTPClient().RawHTTPGet("/blocks/best" + badQueryParams)
+ require.NoError(t, err)
+
+ assert.Equal(t, http.StatusBadRequest, statusCode)
+ assert.Equal(t, "raw: should be boolean", strings.TrimSpace(string(res)))
+}
+
+func testMutuallyExclusiveQueries(t *testing.T) {
+ badQueryParams := "?expanded=true&raw=true"
+ res, statusCode, err := tclient.RawHTTPClient().RawHTTPGet("/blocks/best" + badQueryParams)
+ require.NoError(t, err)
+
+ assert.Equal(t, http.StatusBadRequest, statusCode)
+ assert.Equal(t, "raw&expanded: Raw and Expanded are mutually exclusive", strings.TrimSpace(string(res)))
}
func testGetBestBlock(t *testing.T) {
@@ -80,6 +100,41 @@ func testGetBestBlock(t *testing.T) {
assert.Equal(t, http.StatusOK, statusCode)
}
+func testGetRawBlock(t *testing.T) {
+ res, statusCode, err := tclient.RawHTTPClient().RawHTTPGet("/blocks/best?raw=true")
+ require.NoError(t, err)
+ rawBlock := new(blocks.JSONRawBlockSummary)
+ if err := json.Unmarshal(res, &rawBlock); err != nil {
+ t.Fatal(err)
+ }
+
+ blockBytes, err := hex.DecodeString(rawBlock.Raw[2:len(rawBlock.Raw)])
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ header := block.Header{}
+ err = rlp.DecodeBytes(blockBytes, &header)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ expHeader := blk.Header()
+ assert.Equal(t, expHeader.Number(), header.Number(), "Number should be equal")
+ assert.Equal(t, expHeader.ID(), header.ID(), "Hash should be equal")
+ assert.Equal(t, expHeader.ParentID(), header.ParentID(), "ParentID should be equal")
+ assert.Equal(t, expHeader.Timestamp(), header.Timestamp(), "Timestamp should be equal")
+ assert.Equal(t, expHeader.TotalScore(), header.TotalScore(), "TotalScore should be equal")
+ assert.Equal(t, expHeader.GasLimit(), header.GasLimit(), "GasLimit should be equal")
+ assert.Equal(t, expHeader.GasUsed(), header.GasUsed(), "GasUsed should be equal")
+ assert.Equal(t, expHeader.Beneficiary(), header.Beneficiary(), "Beneficiary should be equal")
+ assert.Equal(t, expHeader.TxsRoot(), header.TxsRoot(), "TxsRoot should be equal")
+ assert.Equal(t, expHeader.StateRoot(), header.StateRoot(), "StateRoot should be equal")
+ assert.Equal(t, expHeader.ReceiptsRoot(), header.ReceiptsRoot(), "ReceiptsRoot should be equal")
+
+ assert.Equal(t, http.StatusOK, statusCode)
+}
+
func testGetBlockByHeight(t *testing.T) {
res, statusCode, err := tclient.RawHTTPClient().RawHTTPGet("/blocks/1")
require.NoError(t, err)
diff --git a/api/blocks/types.go b/api/blocks/types.go
index 38261b2e5..989b63041 100644
--- a/api/blocks/types.go
+++ b/api/blocks/types.go
@@ -33,6 +33,10 @@ type JSONBlockSummary struct {
IsFinalized bool `json:"isFinalized"`
}
+type JSONRawBlockSummary struct {
+ Raw string `json:"raw"`
+}
+
type JSONCollapsedBlock struct {
*JSONBlockSummary
Transactions []thor.Bytes32 `json:"transactions"`
diff --git a/api/doc/thor.yaml b/api/doc/thor.yaml
index 732a5f1a3..dcf0ae6b9 100644
--- a/api/doc/thor.yaml
+++ b/api/doc/thor.yaml
@@ -292,6 +292,7 @@ paths:
parameters:
- $ref: '#/components/parameters/RevisionInPath'
- $ref: '#/components/parameters/ExpandedInQuery'
+ - $ref: '#/components/parameters/RawBlockInQuery'
tags:
- Blocks
summary: Retrieve a block
@@ -2353,6 +2354,18 @@ components:
type: boolean
example: false
+ RawBlockInQuery:
+ name: raw
+ in: query
+ required: false
+ description: |
+ Whether the block should be returned in RLP encoding or not.
+ - `true` returns `block` as an RLP encoded object
+ - `false` returns `block` as a structured JSON object
+ schema:
+ type: boolean
+ example: false
+
PendingInQuery:
name: pending
in: query
diff --git a/api/utils/http.go b/api/utils/http.go
index 652c3e408..2235797de 100644
--- a/api/utils/http.go
+++ b/api/utils/http.go
@@ -9,6 +9,8 @@ import (
"encoding/json"
"io"
"net/http"
+
+ "github.com/pkg/errors"
)
type httpError struct {
@@ -36,6 +38,17 @@ func BadRequest(cause error) error {
}
}
+func StringToBoolean(boolStr string, defaultVal bool) (bool, error) {
+ if boolStr == "" {
+ return defaultVal, nil
+ } else if boolStr == "false" {
+ return false, nil
+ } else if boolStr == "true" {
+ return true, nil
+ }
+ return false, errors.New("should be boolean")
+}
+
// Forbidden convenience method to create http forbidden error.
func Forbidden(cause error) error {
return &httpError{
From 7579db4891fcb98ae4da6fb85a0fd278ed603d39 Mon Sep 17 00:00:00 2001
From: Pedro Gomes
Date: Mon, 9 Dec 2024 10:55:34 +0000
Subject: [PATCH 11/15] Adding Health endpoint (#836)
* Adding Health endpoint
* pr comments + 503 if not healthy
* refactored admin server and api + health endpoint tests
* fix health condition
* fix admin routing
* added comments + changed from ChainSync to ChainBootstrapStatus
* Adding healthcheck for solo mode
* adding solo + tests
* fix log_level handler funcs
* refactor health package + add p2p count
* remove solo methods
* moving health service to api pkg
* added defaults + api health query
* pr comments
* pr comments
* pr comments
* Update cmd/thor/main.go
---
api/admin.go | 62 --------------
api/admin/admin.go | 28 +++++++
api/admin/health/health.go | 84 +++++++++++++++++++
api/admin/health/health_api.go | 68 +++++++++++++++
api/admin/health/health_api_test.go | 59 +++++++++++++
api/admin/health/health_test.go | 71 ++++++++++++++++
api/admin/loglevel/log_level.go | 74 ++++++++++++++++
.../loglevel/log_level_test.go} | 10 ++-
api/admin/loglevel/types.go | 14 ++++
api/admin_server.go | 36 +++-----
api/node/node_test.go | 3 +-
cmd/thor/main.go | 63 +++++++-------
comm/communicator.go | 5 +-
13 files changed, 456 insertions(+), 121 deletions(-)
delete mode 100644 api/admin.go
create mode 100644 api/admin/admin.go
create mode 100644 api/admin/health/health.go
create mode 100644 api/admin/health/health_api.go
create mode 100644 api/admin/health/health_api_test.go
create mode 100644 api/admin/health/health_test.go
create mode 100644 api/admin/loglevel/log_level.go
rename api/{admin_test.go => admin/loglevel/log_level_test.go} (93%)
create mode 100644 api/admin/loglevel/types.go
diff --git a/api/admin.go b/api/admin.go
deleted file mode 100644
index afd299cfa..000000000
--- a/api/admin.go
+++ /dev/null
@@ -1,62 +0,0 @@
-// Copyright (c) 2024 The VeChainThor developers
-
-// Distributed under the GNU Lesser General Public License v3.0 software license, see the accompanying
-// file LICENSE or
-
-package api
-
-import (
- "log/slog"
- "net/http"
-
- "github.com/pkg/errors"
- "github.com/vechain/thor/v2/api/utils"
- "github.com/vechain/thor/v2/log"
-)
-
-type logLevelRequest struct {
- Level string `json:"level"`
-}
-
-type logLevelResponse struct {
- CurrentLevel string `json:"currentLevel"`
-}
-
-func getLogLevelHandler(logLevel *slog.LevelVar) utils.HandlerFunc {
- return func(w http.ResponseWriter, r *http.Request) error {
- return utils.WriteJSON(w, logLevelResponse{
- CurrentLevel: logLevel.Level().String(),
- })
- }
-}
-
-func postLogLevelHandler(logLevel *slog.LevelVar) utils.HandlerFunc {
- return func(w http.ResponseWriter, r *http.Request) error {
- var req logLevelRequest
-
- if err := utils.ParseJSON(r.Body, &req); err != nil {
- return utils.BadRequest(errors.WithMessage(err, "Invalid request body"))
- }
-
- switch req.Level {
- case "debug":
- logLevel.Set(log.LevelDebug)
- case "info":
- logLevel.Set(log.LevelInfo)
- case "warn":
- logLevel.Set(log.LevelWarn)
- case "error":
- logLevel.Set(log.LevelError)
- case "trace":
- logLevel.Set(log.LevelTrace)
- case "crit":
- logLevel.Set(log.LevelCrit)
- default:
- return utils.BadRequest(errors.New("Invalid verbosity level"))
- }
-
- return utils.WriteJSON(w, logLevelResponse{
- CurrentLevel: logLevel.Level().String(),
- })
- }
-}
diff --git a/api/admin/admin.go b/api/admin/admin.go
new file mode 100644
index 000000000..1e16415f8
--- /dev/null
+++ b/api/admin/admin.go
@@ -0,0 +1,28 @@
+// Copyright (c) 2024 The VeChainThor developers
+
+// Distributed under the GNU Lesser General Public License v3.0 software license, see the accompanying
+// file LICENSE or
+
+package admin
+
+import (
+ "log/slog"
+ "net/http"
+
+ "github.com/gorilla/handlers"
+ "github.com/gorilla/mux"
+ healthAPI "github.com/vechain/thor/v2/api/admin/health"
+ "github.com/vechain/thor/v2/api/admin/loglevel"
+)
+
+func New(logLevel *slog.LevelVar, health *healthAPI.Health) http.HandlerFunc {
+ router := mux.NewRouter()
+ subRouter := router.PathPrefix("/admin").Subrouter()
+
+ loglevel.New(logLevel).Mount(subRouter, "/loglevel")
+ healthAPI.NewAPI(health).Mount(subRouter, "/health")
+
+ handler := handlers.CompressHandler(router)
+
+ return handler.ServeHTTP
+}
diff --git a/api/admin/health/health.go b/api/admin/health/health.go
new file mode 100644
index 000000000..41522e32d
--- /dev/null
+++ b/api/admin/health/health.go
@@ -0,0 +1,84 @@
+// Copyright (c) 2024 The VeChainThor developers
+
+// Distributed under the GNU Lesser General Public License v3.0 software license, see the accompanying
+// file LICENSE or
+
+package health
+
+import (
+ "time"
+
+ "github.com/vechain/thor/v2/chain"
+ "github.com/vechain/thor/v2/comm"
+ "github.com/vechain/thor/v2/thor"
+)
+
+type BlockIngestion struct {
+ ID *thor.Bytes32 `json:"id"`
+ Timestamp *time.Time `json:"timestamp"`
+}
+
+type Status struct {
+ Healthy bool `json:"healthy"`
+ BestBlockTime *time.Time `json:"bestBlockTime"`
+ PeerCount int `json:"peerCount"`
+ IsNetworkProgressing bool `json:"isNetworkProgressing"`
+}
+
+type Health struct {
+ repo *chain.Repository
+ p2p *comm.Communicator
+}
+
+const (
+ defaultBlockTolerance = time.Duration(2*thor.BlockInterval) * time.Second // 2 blocks tolerance
+ defaultMinPeerCount = 2
+)
+
+func New(repo *chain.Repository, p2p *comm.Communicator) *Health {
+ return &Health{
+ repo: repo,
+ p2p: p2p,
+ }
+}
+
+// isNetworkProgressing checks if the network is producing new blocks within the allowed interval.
+func (h *Health) isNetworkProgressing(now time.Time, bestBlockTimestamp time.Time, blockTolerance time.Duration) bool {
+ return now.Sub(bestBlockTimestamp) <= blockTolerance
+}
+
+// isNodeConnectedP2P checks if the node is connected to peers
+func (h *Health) isNodeConnectedP2P(peerCount int, minPeerCount int) bool {
+ return peerCount >= minPeerCount
+}
+
+func (h *Health) Status(blockTolerance time.Duration, minPeerCount int) (*Status, error) {
+ // Fetch the best block details
+ bestBlock := h.repo.BestBlockSummary()
+ bestBlockTimestamp := time.Unix(int64(bestBlock.Header.Timestamp()), 0)
+
+ // Fetch the current connected peers
+ var connectedPeerCount int
+ if h.p2p == nil {
+ connectedPeerCount = minPeerCount // ignore peers in solo mode
+ } else {
+ connectedPeerCount = h.p2p.PeerCount()
+ }
+
+ now := time.Now()
+
+ // Perform the checks
+ networkProgressing := h.isNetworkProgressing(now, bestBlockTimestamp, blockTolerance)
+ nodeConnected := h.isNodeConnectedP2P(connectedPeerCount, minPeerCount)
+
+ // Calculate overall health status
+ healthy := networkProgressing && nodeConnected
+
+ // Return the current status
+ return &Status{
+ Healthy: healthy,
+ BestBlockTime: &bestBlockTimestamp,
+ IsNetworkProgressing: networkProgressing,
+ PeerCount: connectedPeerCount,
+ }, nil
+}
diff --git a/api/admin/health/health_api.go b/api/admin/health/health_api.go
new file mode 100644
index 000000000..3bad13f07
--- /dev/null
+++ b/api/admin/health/health_api.go
@@ -0,0 +1,68 @@
+// Copyright (c) 2024 The VeChainThor developers
+
+// Distributed under the GNU Lesser General Public License v3.0 software license, see the accompanying
+// file LICENSE or
+
+package health
+
+import (
+ "net/http"
+ "strconv"
+ "time"
+
+ "github.com/gorilla/mux"
+ "github.com/vechain/thor/v2/api/utils"
+)
+
+type API struct {
+ healthStatus *Health
+}
+
+func NewAPI(healthStatus *Health) *API {
+ return &API{
+ healthStatus: healthStatus,
+ }
+}
+
+func (h *API) handleGetHealth(w http.ResponseWriter, r *http.Request) error {
+ // Parse query parameters
+ query := r.URL.Query()
+
+ // Default to constants if query parameters are not provided
+ blockTolerance := defaultBlockTolerance
+ minPeerCount := defaultMinPeerCount
+
+ // Override with query parameters if they exist
+ if queryBlockTolerance := query.Get("blockTolerance"); queryBlockTolerance != "" {
+ if parsed, err := time.ParseDuration(queryBlockTolerance); err == nil {
+ blockTolerance = parsed
+ }
+ }
+
+ if queryMinPeerCount := query.Get("minPeerCount"); queryMinPeerCount != "" {
+ if parsed, err := strconv.Atoi(queryMinPeerCount); err == nil {
+ minPeerCount = parsed
+ }
+ }
+
+ acc, err := h.healthStatus.Status(blockTolerance, minPeerCount)
+ if err != nil {
+ return err
+ }
+
+ if !acc.Healthy {
+ w.WriteHeader(http.StatusServiceUnavailable) // Set the status to 503
+ } else {
+ w.WriteHeader(http.StatusOK) // Set the status to 200
+ }
+ return utils.WriteJSON(w, acc)
+}
+
+func (h *API) Mount(root *mux.Router, pathPrefix string) {
+ sub := root.PathPrefix(pathPrefix).Subrouter()
+
+ sub.Path("").
+ Methods(http.MethodGet).
+ Name("health").
+ HandlerFunc(utils.WrapHandlerFunc(h.handleGetHealth))
+}
diff --git a/api/admin/health/health_api_test.go b/api/admin/health/health_api_test.go
new file mode 100644
index 000000000..e50af0398
--- /dev/null
+++ b/api/admin/health/health_api_test.go
@@ -0,0 +1,59 @@
+// Copyright (c) 2024 The VeChainThor developers
+
+// Distributed under the GNU Lesser General Public License v3.0 software license, see the accompanying
+// file LICENSE or
+
+package health
+
+import (
+ "encoding/json"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "testing"
+
+ "github.com/gorilla/mux"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "github.com/vechain/thor/v2/comm"
+ "github.com/vechain/thor/v2/test/testchain"
+ "github.com/vechain/thor/v2/txpool"
+)
+
+var ts *httptest.Server
+
+func TestHealth(t *testing.T) {
+ initAPIServer(t)
+
+ var healthStatus Status
+ respBody, statusCode := httpGet(t, ts.URL+"/health")
+ require.NoError(t, json.Unmarshal(respBody, &healthStatus))
+ assert.False(t, healthStatus.Healthy)
+ assert.Equal(t, http.StatusServiceUnavailable, statusCode)
+}
+
+func initAPIServer(t *testing.T) {
+ thorChain, err := testchain.NewIntegrationTestChain()
+ require.NoError(t, err)
+
+ router := mux.NewRouter()
+ NewAPI(
+ New(thorChain.Repo(), comm.New(thorChain.Repo(), txpool.New(thorChain.Repo(), nil, txpool.Options{}))),
+ ).Mount(router, "/health")
+
+ ts = httptest.NewServer(router)
+}
+
+func httpGet(t *testing.T, url string) ([]byte, int) {
+ res, err := http.Get(url) //#nosec G107
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer res.Body.Close()
+
+ r, err := io.ReadAll(res.Body)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return r, res.StatusCode
+}
diff --git a/api/admin/health/health_test.go b/api/admin/health/health_test.go
new file mode 100644
index 000000000..60f9a3dcd
--- /dev/null
+++ b/api/admin/health/health_test.go
@@ -0,0 +1,71 @@
+// Copyright (c) 2024 The VeChainThor developers
+
+// Distributed under the GNU Lesser General Public License v3.0 software license, see the accompanying
+// file LICENSE or
+
+package health
+
+import (
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestHealth_isNetworkProgressing(t *testing.T) {
+ h := &Health{}
+
+ now := time.Now()
+
+ tests := []struct {
+ name string
+ bestBlockTimestamp time.Time
+ expectedProgressing bool
+ }{
+ {
+ name: "Progressing - block within timeBetweenBlocks",
+ bestBlockTimestamp: now.Add(-5 * time.Second),
+ expectedProgressing: true,
+ },
+ {
+ name: "Not Progressing - block outside timeBetweenBlocks",
+ bestBlockTimestamp: now.Add(-25 * time.Second),
+ expectedProgressing: false,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ isProgressing := h.isNetworkProgressing(now, tt.bestBlockTimestamp, defaultBlockTolerance)
+ assert.Equal(t, tt.expectedProgressing, isProgressing, "isNetworkProgressing result mismatch")
+ })
+ }
+}
+
+func TestHealth_isNodeConnectedP2P(t *testing.T) {
+ h := &Health{}
+
+ tests := []struct {
+ name string
+ peerCount int
+ expectedConnected bool
+ }{
+ {
+ name: "Connected - more than one peer",
+ peerCount: 3,
+ expectedConnected: true,
+ },
+ {
+ name: "Not Connected - one or fewer peers",
+ peerCount: 1,
+ expectedConnected: false,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ isConnected := h.isNodeConnectedP2P(tt.peerCount, defaultMinPeerCount)
+ assert.Equal(t, tt.expectedConnected, isConnected, "isNodeConnectedP2P result mismatch")
+ })
+ }
+}
diff --git a/api/admin/loglevel/log_level.go b/api/admin/loglevel/log_level.go
new file mode 100644
index 000000000..d3c339ce2
--- /dev/null
+++ b/api/admin/loglevel/log_level.go
@@ -0,0 +1,74 @@
+// Copyright (c) 2024 The VeChainThor developers
+
+// Distributed under the GNU Lesser General Public License v3.0 software license, see the accompanying
+// file LICENSE or
+
+package loglevel
+
+import (
+ "log/slog"
+ "net/http"
+
+ "github.com/gorilla/mux"
+ "github.com/pkg/errors"
+ "github.com/vechain/thor/v2/api/utils"
+ "github.com/vechain/thor/v2/log"
+)
+
+type LogLevel struct {
+ logLevel *slog.LevelVar
+}
+
+func New(logLevel *slog.LevelVar) *LogLevel {
+ return &LogLevel{
+ logLevel: logLevel,
+ }
+}
+
+func (l *LogLevel) Mount(root *mux.Router, pathPrefix string) {
+ sub := root.PathPrefix(pathPrefix).Subrouter()
+ sub.Path("").
+ Methods(http.MethodGet).
+ Name("get-log-level").
+ HandlerFunc(utils.WrapHandlerFunc(l.getLogLevelHandler))
+
+ sub.Path("").
+ Methods(http.MethodPost).
+ Name("post-log-level").
+ HandlerFunc(utils.WrapHandlerFunc(l.postLogLevelHandler))
+}
+
+func (l *LogLevel) getLogLevelHandler(w http.ResponseWriter, _ *http.Request) error {
+ return utils.WriteJSON(w, Response{
+ CurrentLevel: l.logLevel.Level().String(),
+ })
+}
+
+func (l *LogLevel) postLogLevelHandler(w http.ResponseWriter, r *http.Request) error {
+ var req Request
+
+ if err := utils.ParseJSON(r.Body, &req); err != nil {
+ return utils.BadRequest(errors.WithMessage(err, "Invalid request body"))
+ }
+
+ switch req.Level {
+ case "debug":
+ l.logLevel.Set(log.LevelDebug)
+ case "info":
+ l.logLevel.Set(log.LevelInfo)
+ case "warn":
+ l.logLevel.Set(log.LevelWarn)
+ case "error":
+ l.logLevel.Set(log.LevelError)
+ case "trace":
+ l.logLevel.Set(log.LevelTrace)
+ case "crit":
+ l.logLevel.Set(log.LevelCrit)
+ default:
+ return utils.BadRequest(errors.New("Invalid verbosity level"))
+ }
+
+ return utils.WriteJSON(w, Response{
+ CurrentLevel: l.logLevel.Level().String(),
+ })
+}
diff --git a/api/admin_test.go b/api/admin/loglevel/log_level_test.go
similarity index 93%
rename from api/admin_test.go
rename to api/admin/loglevel/log_level_test.go
index be2847cbf..3d1a8a960 100644
--- a/api/admin_test.go
+++ b/api/admin/loglevel/log_level_test.go
@@ -3,7 +3,7 @@
// Distributed under the GNU Lesser General Public License v3.0 software license, see the accompanying
// file LICENSE or
-package api
+package loglevel
import (
"bytes"
@@ -14,6 +14,7 @@ import (
"strings"
"testing"
+ "github.com/gorilla/mux"
"github.com/stretchr/testify/assert"
)
@@ -76,15 +77,16 @@ func TestLogLevelHandler(t *testing.T) {
}
rr := httptest.NewRecorder()
- handler := http.HandlerFunc(HTTPHandler(&logLevel).ServeHTTP)
- handler.ServeHTTP(rr, req)
+ router := mux.NewRouter()
+ New(&logLevel).Mount(router, "/admin/loglevel")
+ router.ServeHTTP(rr, req)
if status := rr.Code; status != tt.expectedStatus {
t.Errorf("handler returned wrong status code: got %v want %v", status, tt.expectedStatus)
}
if tt.expectedLevel != "" {
- var response logLevelResponse
+ var response Response
if err := json.NewDecoder(rr.Body).Decode(&response); err != nil {
t.Fatalf("could not decode response: %v", err)
}
diff --git a/api/admin/loglevel/types.go b/api/admin/loglevel/types.go
new file mode 100644
index 000000000..ce57187b1
--- /dev/null
+++ b/api/admin/loglevel/types.go
@@ -0,0 +1,14 @@
+// Copyright (c) 2024 The VeChainThor developers
+
+// Distributed under the GNU Lesser General Public License v3.0 software license, see the accompanying
+// file LICENSE or
+
+package loglevel
+
+type Request struct {
+ Level string `json:"level"`
+}
+
+type Response struct {
+ CurrentLevel string `json:"currentLevel"`
+}
diff --git a/api/admin_server.go b/api/admin_server.go
index 26054e908..f2315aeb1 100644
--- a/api/admin_server.go
+++ b/api/admin_server.go
@@ -11,40 +11,28 @@ import (
"net/http"
"time"
- "github.com/gorilla/handlers"
- "github.com/gorilla/mux"
"github.com/pkg/errors"
- "github.com/vechain/thor/v2/api/utils"
+ "github.com/vechain/thor/v2/api/admin"
+ "github.com/vechain/thor/v2/api/admin/health"
+ "github.com/vechain/thor/v2/chain"
"github.com/vechain/thor/v2/co"
+ "github.com/vechain/thor/v2/comm"
)
-func HTTPHandler(logLevel *slog.LevelVar) http.Handler {
- router := mux.NewRouter()
- sub := router.PathPrefix("/admin").Subrouter()
- sub.Path("/loglevel").
- Methods(http.MethodGet).
- Name("get-log-level").
- HandlerFunc(utils.WrapHandlerFunc(getLogLevelHandler(logLevel)))
-
- sub.Path("/loglevel").
- Methods(http.MethodPost).
- Name("post-log-level").
- HandlerFunc(utils.WrapHandlerFunc(postLogLevelHandler(logLevel)))
-
- return handlers.CompressHandler(router)
-}
-
-func StartAdminServer(addr string, logLevel *slog.LevelVar) (string, func(), error) {
+func StartAdminServer(
+ addr string,
+ logLevel *slog.LevelVar,
+ repo *chain.Repository,
+ p2p *comm.Communicator,
+) (string, func(), error) {
listener, err := net.Listen("tcp", addr)
if err != nil {
return "", nil, errors.Wrapf(err, "listen admin API addr [%v]", addr)
}
- router := mux.NewRouter()
- router.PathPrefix("/admin").Handler(HTTPHandler(logLevel))
- handler := handlers.CompressHandler(router)
+ adminHandler := admin.New(logLevel, health.New(repo, p2p))
- srv := &http.Server{Handler: handler, ReadHeaderTimeout: time.Second, ReadTimeout: 5 * time.Second}
+ srv := &http.Server{Handler: adminHandler, ReadHeaderTimeout: time.Second, ReadTimeout: 5 * time.Second}
var goes co.Goes
goes.Go(func() {
srv.Serve(listener)
diff --git a/api/node/node_test.go b/api/node/node_test.go
index 3dd2e96ee..873ad29ad 100644
--- a/api/node/node_test.go
+++ b/api/node/node_test.go
@@ -40,7 +40,8 @@ func initCommServer(t *testing.T) {
Limit: 10000,
LimitPerAccount: 16,
MaxLifetime: 10 * time.Minute,
- }))
+ }),
+ )
router := mux.NewRouter()
node.New(communicator).Mount(router, "/node")
diff --git a/cmd/thor/main.go b/cmd/thor/main.go
index 15531e091..ba19b0c11 100644
--- a/cmd/thor/main.go
+++ b/cmd/thor/main.go
@@ -180,16 +180,6 @@ func defaultAction(ctx *cli.Context) error {
defer func() { log.Info("stopping metrics server..."); closeFunc() }()
}
- adminURL := ""
- if ctx.Bool(enableAdminFlag.Name) {
- url, closeFunc, err := api.StartAdminServer(ctx.String(adminAddrFlag.Name), logLevel)
- if err != nil {
- return fmt.Errorf("unable to start admin server - %w", err)
- }
- adminURL = url
- defer func() { log.Info("stopping admin server..."); closeFunc() }()
- }
-
gene, forkConfig, err := selectGenesis(ctx)
if err != nil {
return err
@@ -243,6 +233,21 @@ func defaultAction(ctx *cli.Context) error {
return err
}
+ adminURL := ""
+ if ctx.Bool(enableAdminFlag.Name) {
+ url, closeFunc, err := api.StartAdminServer(
+ ctx.String(adminAddrFlag.Name),
+ logLevel,
+ repo,
+ p2pCommunicator.Communicator(),
+ )
+ if err != nil {
+ return fmt.Errorf("unable to start admin server - %w", err)
+ }
+ adminURL = url
+ defer func() { log.Info("stopping admin server..."); closeFunc() }()
+ }
+
bftEngine, err := bft.NewEngine(repo, mainDB, forkConfig, master.Address())
if err != nil {
return errors.Wrap(err, "init bft engine")
@@ -287,7 +292,8 @@ func defaultAction(ctx *cli.Context) error {
p2pCommunicator.Communicator(),
ctx.Uint64(targetGasLimitFlag.Name),
skipLogs,
- forkConfig).Run(exitSignal)
+ forkConfig,
+ ).Run(exitSignal)
}
func soloAction(ctx *cli.Context) error {
@@ -301,6 +307,12 @@ func soloAction(ctx *cli.Context) error {
logLevel := initLogger(lvl, ctx.Bool(jsonLogsFlag.Name))
+ onDemandBlockProduction := ctx.Bool(onDemandFlag.Name)
+ blockProductionInterval := ctx.Uint64(blockInterval.Name)
+ if blockProductionInterval == 0 {
+ return errors.New("block-interval cannot be zero")
+ }
+
// enable metrics as soon as possible
metricsURL := ""
if ctx.Bool(enableMetricsFlag.Name) {
@@ -313,16 +325,6 @@ func soloAction(ctx *cli.Context) error {
defer func() { log.Info("stopping metrics server..."); closeFunc() }()
}
- adminURL := ""
- if ctx.Bool(enableAdminFlag.Name) {
- url, closeFunc, err := api.StartAdminServer(ctx.String(adminAddrFlag.Name), logLevel)
- if err != nil {
- return fmt.Errorf("unable to start admin server - %w", err)
- }
- adminURL = url
- defer func() { log.Info("stopping admin server..."); closeFunc() }()
- }
-
var (
gene *genesis.Genesis
forkConfig thor.ForkConfig
@@ -367,6 +369,16 @@ func soloAction(ctx *cli.Context) error {
return err
}
+ adminURL := ""
+ if ctx.Bool(enableAdminFlag.Name) {
+ url, closeFunc, err := api.StartAdminServer(ctx.String(adminAddrFlag.Name), logLevel, repo, nil)
+ if err != nil {
+ return fmt.Errorf("unable to start admin server - %w", err)
+ }
+ adminURL = url
+ defer func() { log.Info("stopping admin server..."); closeFunc() }()
+ }
+
printStartupMessage1(gene, repo, nil, instanceDir, forkConfig)
skipLogs := ctx.Bool(skipLogsFlag.Name)
@@ -412,11 +424,6 @@ func soloAction(ctx *cli.Context) error {
srvCloser()
}()
- blockInterval := ctx.Uint64(blockInterval.Name)
- if blockInterval == 0 {
- return errors.New("block-interval cannot be zero")
- }
-
printStartupMessage2(gene, apiURL, "", metricsURL, adminURL)
optimizer := optimizer.New(mainDB, repo, !ctx.Bool(disablePrunerFlag.Name))
@@ -427,9 +434,9 @@ func soloAction(ctx *cli.Context) error {
logDB,
txPool,
ctx.Uint64(gasLimitFlag.Name),
- ctx.Bool(onDemandFlag.Name),
+ onDemandBlockProduction,
skipLogs,
- blockInterval,
+ blockProductionInterval,
forkConfig).Run(exitSignal)
}
diff --git a/comm/communicator.go b/comm/communicator.go
index 9d0a5a530..48419779a 100644
--- a/comm/communicator.go
+++ b/comm/communicator.go
@@ -72,7 +72,7 @@ func (c *Communicator) Sync(ctx context.Context, handler HandleBlockStream) {
delay := initSyncInterval
syncCount := 0
- shouldSynced := func() bool {
+ isSynced := func() bool {
bestBlockTime := c.repo.BestBlockSummary().Header.Timestamp()
now := uint64(time.Now().Unix())
if bestBlockTime+thor.BlockInterval >= now {
@@ -115,9 +115,10 @@ func (c *Communicator) Sync(ctx context.Context, handler HandleBlockStream) {
}
syncCount++
- if shouldSynced() {
+ if isSynced() {
delay = syncInterval
c.onceSynced.Do(func() {
+ // once off - after a bootstrap the syncedCh trigger the peers.syncTxs
close(c.syncedCh)
})
}
From de248a60128e278d9a8209ac0982f339483a4d46 Mon Sep 17 00:00:00 2001
From: Darren Kelly <107671032+darrenvechain@users.noreply.github.com>
Date: Mon, 9 Dec 2024 11:25:30 +0000
Subject: [PATCH 12/15] Darren/admin api log toggler (#877)
* Adding Health endpoint
* pr comments + 503 if not healthy
* refactored admin server and api + health endpoint tests
* fix health condition
* fix admin routing
* added comments + changed from ChainSync to ChainBootstrapStatus
* Adding healthcheck for solo mode
* adding solo + tests
* fix log_level handler funcs
* feat(admin): toggle api logs via admin API
* feat(admin): add license headers
* refactor health package + add p2p count
* remove solo methods
* moving health service to api pkg
* added defaults + api health query
* pr comments
* pr comments
---------
Co-authored-by: otherview
---
api/admin/admin.go | 8 ++-
api/admin/apilogs/api_logs.go | 70 +++++++++++++++++++++++
api/admin/apilogs/api_logs_test.go | 91 ++++++++++++++++++++++++++++++
api/admin/loglevel/log_level.go | 2 +
api/admin_server.go | 4 +-
api/api.go | 7 +--
api/request_logger.go | 7 ++-
api/request_logger_test.go | 5 +-
cmd/thor/main.go | 23 +++++++-
cmd/thor/utils.go | 5 +-
10 files changed, 208 insertions(+), 14 deletions(-)
create mode 100644 api/admin/apilogs/api_logs.go
create mode 100644 api/admin/apilogs/api_logs_test.go
diff --git a/api/admin/admin.go b/api/admin/admin.go
index 1e16415f8..9b819c875 100644
--- a/api/admin/admin.go
+++ b/api/admin/admin.go
@@ -8,19 +8,23 @@ package admin
import (
"log/slog"
"net/http"
+ "sync/atomic"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
- healthAPI "github.com/vechain/thor/v2/api/admin/health"
+ "github.com/vechain/thor/v2/api/admin/apilogs"
"github.com/vechain/thor/v2/api/admin/loglevel"
+
+ healthAPI "github.com/vechain/thor/v2/api/admin/health"
)
-func New(logLevel *slog.LevelVar, health *healthAPI.Health) http.HandlerFunc {
+func New(logLevel *slog.LevelVar, health *healthAPI.Health, apiLogsToggle *atomic.Bool) http.HandlerFunc {
router := mux.NewRouter()
subRouter := router.PathPrefix("/admin").Subrouter()
loglevel.New(logLevel).Mount(subRouter, "/loglevel")
healthAPI.NewAPI(health).Mount(subRouter, "/health")
+ apilogs.New(apiLogsToggle).Mount(subRouter, "/apilogs")
handler := handlers.CompressHandler(router)
diff --git a/api/admin/apilogs/api_logs.go b/api/admin/apilogs/api_logs.go
new file mode 100644
index 000000000..0f815d579
--- /dev/null
+++ b/api/admin/apilogs/api_logs.go
@@ -0,0 +1,70 @@
+// Copyright (c) 2024 The VeChainThor developers
+//
+// Distributed under the GNU Lesser General Public License v3.0 software license, see the accompanying
+// file LICENSE or
+
+package apilogs
+
+import (
+ "net/http"
+ "sync"
+ "sync/atomic"
+
+ "github.com/gorilla/mux"
+ "github.com/vechain/thor/v2/api/utils"
+ "github.com/vechain/thor/v2/log"
+)
+
+type APILogs struct {
+ enabled *atomic.Bool
+ mu sync.Mutex
+}
+
+type Status struct {
+ Enabled bool `json:"enabled"`
+}
+
+func New(enabled *atomic.Bool) *APILogs {
+ return &APILogs{
+ enabled: enabled,
+ }
+}
+
+func (a *APILogs) Mount(root *mux.Router, pathPrefix string) {
+ sub := root.PathPrefix(pathPrefix).Subrouter()
+ sub.Path("").
+ Methods(http.MethodGet).
+ Name("get-api-logs-enabled").
+ HandlerFunc(utils.WrapHandlerFunc(a.areAPILogsEnabled))
+
+ sub.Path("").
+ Methods(http.MethodPost).
+ Name("post-api-logs-enabled").
+ HandlerFunc(utils.WrapHandlerFunc(a.setAPILogsEnabled))
+}
+
+func (a *APILogs) areAPILogsEnabled(w http.ResponseWriter, _ *http.Request) error {
+ a.mu.Lock()
+ defer a.mu.Unlock()
+
+ return utils.WriteJSON(w, Status{
+ Enabled: a.enabled.Load(),
+ })
+}
+
+func (a *APILogs) setAPILogsEnabled(w http.ResponseWriter, r *http.Request) error {
+ a.mu.Lock()
+ defer a.mu.Unlock()
+
+ var req Status
+ if err := utils.ParseJSON(r.Body, &req); err != nil {
+ return utils.BadRequest(err)
+ }
+ a.enabled.Store(req.Enabled)
+
+ log.Info("api logs updated", "pkg", "apilogs", "enabled", req.Enabled)
+
+ return utils.WriteJSON(w, Status{
+ Enabled: a.enabled.Load(),
+ })
+}
diff --git a/api/admin/apilogs/api_logs_test.go b/api/admin/apilogs/api_logs_test.go
new file mode 100644
index 000000000..95cf2c6ac
--- /dev/null
+++ b/api/admin/apilogs/api_logs_test.go
@@ -0,0 +1,91 @@
+// Copyright (c) 2024 The VeChainThor developers
+//
+// Distributed under the GNU Lesser General Public License v3.0 software license, see the accompanying
+// file LICENSE or
+
+package apilogs
+
+import (
+ "bytes"
+ "encoding/json"
+ "net/http"
+ "net/http/httptest"
+ "sync/atomic"
+ "testing"
+
+ "github.com/gorilla/mux"
+ "github.com/stretchr/testify/assert"
+)
+
+type TestCase struct {
+ name string
+ method string
+ expectedHTTP int
+ startValue bool
+ expectedEndValue bool
+ requestBody bool
+}
+
+func marshalBody(tt TestCase, t *testing.T) []byte {
+ var reqBody []byte
+ var err error
+ if tt.method == "POST" {
+ reqBody, err = json.Marshal(Status{Enabled: tt.requestBody})
+ if err != nil {
+ t.Fatalf("could not marshal request body: %v", err)
+ }
+ }
+ return reqBody
+}
+
+func TestLogLevelHandler(t *testing.T) {
+ tests := []TestCase{
+ {
+ name: "Valid POST input - set logs to enabled",
+ method: "POST",
+ expectedHTTP: http.StatusOK,
+ startValue: false,
+ requestBody: true,
+ expectedEndValue: true,
+ },
+ {
+ name: "Valid POST input - set logs to disabled",
+ method: "POST",
+ expectedHTTP: http.StatusOK,
+ startValue: true,
+ requestBody: false,
+ expectedEndValue: false,
+ },
+ {
+ name: "GET request - get current level INFO",
+ method: "GET",
+ expectedHTTP: http.StatusOK,
+ startValue: true,
+ expectedEndValue: true,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ logLevel := atomic.Bool{}
+ logLevel.Store(tt.startValue)
+
+ reqBodyBytes := marshalBody(tt, t)
+
+ req, err := http.NewRequest(tt.method, "/admin/apilogs", bytes.NewBuffer(reqBodyBytes))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ rr := httptest.NewRecorder()
+ router := mux.NewRouter()
+ New(&logLevel).Mount(router, "/admin/apilogs")
+ router.ServeHTTP(rr, req)
+
+ assert.Equal(t, tt.expectedHTTP, rr.Code)
+ responseBody := Status{}
+ assert.NoError(t, json.Unmarshal(rr.Body.Bytes(), &responseBody))
+ assert.Equal(t, tt.expectedEndValue, responseBody.Enabled)
+ })
+ }
+}
diff --git a/api/admin/loglevel/log_level.go b/api/admin/loglevel/log_level.go
index d3c339ce2..c91702d2d 100644
--- a/api/admin/loglevel/log_level.go
+++ b/api/admin/loglevel/log_level.go
@@ -68,6 +68,8 @@ func (l *LogLevel) postLogLevelHandler(w http.ResponseWriter, r *http.Request) e
return utils.BadRequest(errors.New("Invalid verbosity level"))
}
+ log.Info("log level changed", "pkg", "loglevel", "level", l.logLevel.Level().String())
+
return utils.WriteJSON(w, Response{
CurrentLevel: l.logLevel.Level().String(),
})
diff --git a/api/admin_server.go b/api/admin_server.go
index f2315aeb1..dca428b36 100644
--- a/api/admin_server.go
+++ b/api/admin_server.go
@@ -9,6 +9,7 @@ import (
"log/slog"
"net"
"net/http"
+ "sync/atomic"
"time"
"github.com/pkg/errors"
@@ -24,13 +25,14 @@ func StartAdminServer(
logLevel *slog.LevelVar,
repo *chain.Repository,
p2p *comm.Communicator,
+ apiLogs *atomic.Bool,
) (string, func(), error) {
listener, err := net.Listen("tcp", addr)
if err != nil {
return "", nil, errors.Wrapf(err, "listen admin API addr [%v]", addr)
}
- adminHandler := admin.New(logLevel, health.New(repo, p2p))
+ adminHandler := admin.New(logLevel, health.New(repo, p2p), apiLogs)
srv := &http.Server{Handler: adminHandler, ReadHeaderTimeout: time.Second, ReadTimeout: 5 * time.Second}
var goes co.Goes
diff --git a/api/api.go b/api/api.go
index 0385929ec..c57e2a957 100644
--- a/api/api.go
+++ b/api/api.go
@@ -9,6 +9,7 @@ import (
"net/http"
"net/http/pprof"
"strings"
+ "sync/atomic"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
@@ -39,7 +40,7 @@ type Config struct {
PprofOn bool
SkipLogs bool
AllowCustomTracer bool
- EnableReqLogger bool
+ EnableReqLogger *atomic.Bool
EnableMetrics bool
LogsLimit uint64
AllowedTracers []string
@@ -115,9 +116,7 @@ func New(
handlers.ExposedHeaders([]string{"x-genesis-id", "x-thorest-ver"}),
)(handler)
- if config.EnableReqLogger {
- handler = RequestLoggerHandler(handler, logger)
- }
+ handler = RequestLoggerHandler(handler, logger, config.EnableReqLogger)
return handler.ServeHTTP, subs.Close // subscriptions handles hijacked conns, which need to be closed
}
diff --git a/api/request_logger.go b/api/request_logger.go
index 3d48a2d36..451059814 100644
--- a/api/request_logger.go
+++ b/api/request_logger.go
@@ -9,14 +9,19 @@ import (
"bytes"
"io"
"net/http"
+ "sync/atomic"
"time"
"github.com/vechain/thor/v2/log"
)
// RequestLoggerHandler returns a http handler to ensure requests are syphoned into the writer
-func RequestLoggerHandler(handler http.Handler, logger log.Logger) http.Handler {
+func RequestLoggerHandler(handler http.Handler, logger log.Logger, enabled *atomic.Bool) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
+ if !enabled.Load() {
+ handler.ServeHTTP(w, r)
+ return
+ }
// Read and log the body (note: this can only be done once)
// Ensure you don't disrupt the request body for handlers that need to read it
var bodyBytes []byte
diff --git a/api/request_logger_test.go b/api/request_logger_test.go
index 6b8ddcd91..3368e6fc8 100644
--- a/api/request_logger_test.go
+++ b/api/request_logger_test.go
@@ -10,6 +10,7 @@ import (
"net/http"
"net/http/httptest"
"strings"
+ "sync/atomic"
"testing"
"github.com/stretchr/testify/assert"
@@ -59,6 +60,8 @@ func (m *mockLogger) GetLoggedData() []interface{} {
func TestRequestLoggerHandler(t *testing.T) {
mockLog := &mockLogger{}
+ enabled := atomic.Bool{}
+ enabled.Store(true)
// Define a test handler to wrap
testHandler := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
@@ -67,7 +70,7 @@ func TestRequestLoggerHandler(t *testing.T) {
})
// Create the RequestLoggerHandler
- loggerHandler := RequestLoggerHandler(testHandler, mockLog)
+ loggerHandler := RequestLoggerHandler(testHandler, mockLog, &enabled)
// Create a test HTTP request
reqBody := "test body"
diff --git a/cmd/thor/main.go b/cmd/thor/main.go
index ba19b0c11..2cb638f2a 100644
--- a/cmd/thor/main.go
+++ b/cmd/thor/main.go
@@ -11,6 +11,7 @@ import (
"io"
"os"
"path/filepath"
+ "sync/atomic"
"time"
"github.com/ethereum/go-ethereum/accounts/keystore"
@@ -234,12 +235,15 @@ func defaultAction(ctx *cli.Context) error {
}
adminURL := ""
+ logAPIRequests := &atomic.Bool{}
+ logAPIRequests.Store(ctx.Bool(enableAPILogsFlag.Name))
if ctx.Bool(enableAdminFlag.Name) {
url, closeFunc, err := api.StartAdminServer(
ctx.String(adminAddrFlag.Name),
logLevel,
repo,
p2pCommunicator.Communicator(),
+ logAPIRequests,
)
if err != nil {
return fmt.Errorf("unable to start admin server - %w", err)
@@ -261,7 +265,7 @@ func defaultAction(ctx *cli.Context) error {
bftEngine,
p2pCommunicator.Communicator(),
forkConfig,
- makeAPIConfig(ctx, false),
+ makeAPIConfig(ctx, logAPIRequests, false),
)
defer func() { log.Info("closing API..."); apiCloser() }()
@@ -370,8 +374,16 @@ func soloAction(ctx *cli.Context) error {
}
adminURL := ""
+ logAPIRequests := &atomic.Bool{}
+ logAPIRequests.Store(ctx.Bool(enableAPILogsFlag.Name))
if ctx.Bool(enableAdminFlag.Name) {
- url, closeFunc, err := api.StartAdminServer(ctx.String(adminAddrFlag.Name), logLevel, repo, nil)
+ url, closeFunc, err := api.StartAdminServer(
+ ctx.String(adminAddrFlag.Name),
+ logLevel,
+ repo,
+ nil,
+ logAPIRequests,
+ )
if err != nil {
return fmt.Errorf("unable to start admin server - %w", err)
}
@@ -411,7 +423,7 @@ func soloAction(ctx *cli.Context) error {
bftEngine,
&solo.Communicator{},
forkConfig,
- makeAPIConfig(ctx, true),
+ makeAPIConfig(ctx, logAPIRequests, true),
)
defer func() { log.Info("closing API..."); apiCloser() }()
@@ -424,6 +436,11 @@ func soloAction(ctx *cli.Context) error {
srvCloser()
}()
+ blockInterval := ctx.Uint64(blockInterval.Name)
+ if blockInterval == 0 {
+ return errors.New("block-interval cannot be zero")
+ }
+
printStartupMessage2(gene, apiURL, "", metricsURL, adminURL)
optimizer := optimizer.New(mainDB, repo, !ctx.Bool(disablePrunerFlag.Name))
diff --git a/cmd/thor/utils.go b/cmd/thor/utils.go
index 6877a45ee..59c5ec284 100644
--- a/cmd/thor/utils.go
+++ b/cmd/thor/utils.go
@@ -23,6 +23,7 @@ import (
"runtime"
"runtime/debug"
"strings"
+ "sync/atomic"
"syscall"
"time"
@@ -275,7 +276,7 @@ func parseGenesisFile(filePath string) (*genesis.Genesis, thor.ForkConfig, error
return customGen, forkConfig, nil
}
-func makeAPIConfig(ctx *cli.Context, soloMode bool) api.Config {
+func makeAPIConfig(ctx *cli.Context, logAPIRequests *atomic.Bool, soloMode bool) api.Config {
return api.Config{
AllowedOrigins: ctx.String(apiCorsFlag.Name),
BacktraceLimit: uint32(ctx.Uint64(apiBacktraceLimitFlag.Name)),
@@ -283,7 +284,7 @@ func makeAPIConfig(ctx *cli.Context, soloMode bool) api.Config {
PprofOn: ctx.Bool(pprofFlag.Name),
SkipLogs: ctx.Bool(skipLogsFlag.Name),
AllowCustomTracer: ctx.Bool(apiAllowCustomTracerFlag.Name),
- EnableReqLogger: ctx.Bool(enableAPILogsFlag.Name),
+ EnableReqLogger: logAPIRequests,
EnableMetrics: ctx.Bool(enableMetricsFlag.Name),
LogsLimit: ctx.Uint64(apiLogsLimitFlag.Name),
AllowedTracers: parseTracerList(strings.TrimSpace(ctx.String(allowedTracersFlag.Name))),
From d9a11a8590a91556c0459724527b11c6d189b290 Mon Sep 17 00:00:00 2001
From: Darren Kelly <107671032+darrenvechain@users.noreply.github.com>
Date: Mon, 9 Dec 2024 11:33:50 +0000
Subject: [PATCH 13/15] Darren/chore/backport metrics (#909)
* chore(muxdb): backport muxdb cache metrics
* chore(muxdb): backport muxdb cache metrics
* chore(metrics): backport disk IO
* chore(metrics): fix lint
* chore(chain): add repo cache metrics
* fix(chain): fix cache return value
* refactor(chain): cache hit miss
---
chain/metric.go | 12 +++++++
chain/repository.go | 24 +++++++++----
metrics/noop.go | 4 ++-
metrics/prometheus.go | 65 ++++++++++++++++++++++++++++++++++
muxdb/internal/trie/cache.go | 20 ++++++-----
muxdb/internal/trie/metrics.go | 12 +++++++
6 files changed, 121 insertions(+), 16 deletions(-)
create mode 100644 chain/metric.go
create mode 100644 muxdb/internal/trie/metrics.go
diff --git a/chain/metric.go b/chain/metric.go
new file mode 100644
index 000000000..8c9a764d4
--- /dev/null
+++ b/chain/metric.go
@@ -0,0 +1,12 @@
+// Copyright (c) 2024 The VeChainThor developers
+//
+// Distributed under the GNU Lesser General Public License v3.0 software license, see the accompanying
+// file LICENSE or
+
+package chain
+
+import "github.com/vechain/thor/v2/metrics"
+
+var (
+ metricCacheHitMiss = metrics.LazyLoadCounterVec("repo_cache_hit_miss_count", []string{"type", "event"})
+)
diff --git a/chain/repository.go b/chain/repository.go
index 2460d6a3c..81b1f6af4 100644
--- a/chain/repository.go
+++ b/chain/repository.go
@@ -322,23 +322,29 @@ func (r *Repository) GetMaxBlockNum() (uint32, error) {
// GetBlockSummary get block summary by block id.
func (r *Repository) GetBlockSummary(id thor.Bytes32) (summary *BlockSummary, err error) {
- var cached interface{}
- if cached, err = r.caches.summaries.GetOrLoad(id, func() (interface{}, error) {
+ var blk interface{}
+ result := "hit"
+ if blk, err = r.caches.summaries.GetOrLoad(id, func() (interface{}, error) {
+ result = "miss"
return loadBlockSummary(r.data, id)
}); err != nil {
return
}
- return cached.(*BlockSummary), nil
+ metricCacheHitMiss().AddWithLabel(1, map[string]string{"type": "blocks", "event": result})
+ return blk.(*BlockSummary), nil
}
func (r *Repository) getTransaction(key txKey) (*tx.Transaction, error) {
- cached, err := r.caches.txs.GetOrLoad(key, func() (interface{}, error) {
+ result := "hit"
+ trx, err := r.caches.txs.GetOrLoad(key, func() (interface{}, error) {
+ result = "miss"
return loadTransaction(r.data, key)
})
if err != nil {
return nil, err
}
- return cached.(*tx.Transaction), nil
+ metricCacheHitMiss().AddWithLabel(1, map[string]string{"type": "transaction", "event": result})
+ return trx.(*tx.Transaction), nil
}
// GetBlockTransactions get all transactions of the block for given block id.
@@ -377,13 +383,17 @@ func (r *Repository) GetBlock(id thor.Bytes32) (*block.Block, error) {
}
func (r *Repository) getReceipt(key txKey) (*tx.Receipt, error) {
- cached, err := r.caches.receipts.GetOrLoad(key, func() (interface{}, error) {
+ result := "hit"
+ receipt, err := r.caches.receipts.GetOrLoad(key, func() (interface{}, error) {
+ result = "miss"
return loadReceipt(r.data, key)
})
if err != nil {
return nil, err
}
- return cached.(*tx.Receipt), nil
+ metricCacheHitMiss().AddWithLabel(1, map[string]string{"type": "receipt", "event": result})
+
+ return receipt.(*tx.Receipt), nil
}
// GetBlockReceipts get all tx receipts of the block for given block id.
diff --git a/metrics/noop.go b/metrics/noop.go
index 6eb909ff9..a9e24ab2c 100644
--- a/metrics/noop.go
+++ b/metrics/noop.go
@@ -5,7 +5,9 @@
package metrics
-import "net/http"
+import (
+ "net/http"
+)
// noopMetrics implements a no operations metrics service
type noopMetrics struct{}
diff --git a/metrics/prometheus.go b/metrics/prometheus.go
index 50745752c..15447f6dc 100644
--- a/metrics/prometheus.go
+++ b/metrics/prometheus.go
@@ -6,8 +6,15 @@
package metrics
import (
+ "bufio"
+ "fmt"
"net/http"
+ "os"
+ "runtime"
+ "strconv"
+ "strings"
"sync"
+ "time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
@@ -24,6 +31,8 @@ func InitializePrometheusMetrics() {
// don't allow for reset
if _, ok := metrics.(*prometheusMetrics); !ok {
metrics = newPrometheusMetrics()
+ // collection disk io metrics every 5 seconds
+ go metrics.(*prometheusMetrics).collectDiskIO(5 * time.Second)
}
}
@@ -123,6 +132,62 @@ func (o *prometheusMetrics) GetOrCreateGaugeVecMeter(name string, labels []strin
return meter
}
+func getIOLineValue(line string) int64 {
+ fields := strings.Fields(line)
+ if len(fields) != 2 {
+ logger.Warn("this io file line is malformed", "err", line)
+ return 0
+ }
+ value, err := strconv.ParseInt(fields[1], 10, 64)
+ if err != nil {
+ logger.Warn("unable to parse int", "err", err)
+ return 0
+ }
+
+ return value
+}
+
+func getDiskIOData() (int64, int64, error) {
+ pid := os.Getpid()
+ ioFilePath := fmt.Sprintf("/proc/%d/io", pid)
+ file, err := os.Open(ioFilePath)
+ if err != nil {
+ return 0, 0, err
+ }
+
+ // Parse the file line by line
+ scanner := bufio.NewScanner(file)
+ var reads, writes int64
+ for scanner.Scan() {
+ line := scanner.Text()
+ if strings.HasPrefix(line, "syscr") {
+ reads = getIOLineValue(line)
+ } else if strings.HasPrefix(line, "syscw") {
+ writes = getIOLineValue(line)
+ }
+ }
+
+ return reads, writes, nil
+}
+
+func (o *prometheusMetrics) collectDiskIO(refresh time.Duration) {
+ if runtime.GOOS != "linux" {
+ return
+ }
+ for {
+ reads, writes, err := getDiskIOData()
+ if err == nil {
+ readsMeter := o.GetOrCreateGaugeMeter("disk_reads")
+ readsMeter.Set(reads)
+
+ writesMeter := o.GetOrCreateGaugeMeter("disk_writes")
+ writesMeter.Set(writes)
+ }
+
+ time.Sleep(refresh)
+ }
+}
+
func (o *prometheusMetrics) newHistogramMeter(name string, buckets []int64) HistogramMeter {
var floatBuckets []float64
for _, bucket := range buckets {
diff --git a/muxdb/internal/trie/cache.go b/muxdb/internal/trie/cache.go
index cc7bca300..d7d78aaae 100644
--- a/muxdb/internal/trie/cache.go
+++ b/muxdb/internal/trie/cache.go
@@ -45,12 +45,16 @@ func (c *Cache) log() {
last := atomic.SwapInt64(&c.lastLogTime, now)
if now-last > int64(time.Second*20) {
- log1, ok1 := c.nodeStats.ShouldLog("node cache stats")
- log2, ok2 := c.rootStats.ShouldLog("root cache stats")
-
- if ok1 || ok2 {
- log1()
- log2()
+ logNode, hitNode, missNode, okNode := c.nodeStats.shouldLog("node cache stats")
+ logRoot, hitRoot, missRoot, okRoot := c.rootStats.shouldLog("root cache stats")
+
+ if okNode || okRoot {
+ logNode()
+ metricCacheHitMissGaugeVec().SetWithLabel(hitNode, map[string]string{"type": "node", "event": "hit"})
+ metricCacheHitMissGaugeVec().SetWithLabel(missNode, map[string]string{"type": "node", "event": "miss"})
+ logRoot()
+ metricCacheHitMissGaugeVec().SetWithLabel(hitRoot, map[string]string{"type": "root", "event": "hit"})
+ metricCacheHitMissGaugeVec().SetWithLabel(missRoot, map[string]string{"type": "root", "event": "miss"})
}
} else {
atomic.CompareAndSwapInt64(&c.lastLogTime, now, last)
@@ -189,7 +193,7 @@ type cacheStats struct {
func (cs *cacheStats) Hit() int64 { return atomic.AddInt64(&cs.hit, 1) }
func (cs *cacheStats) Miss() int64 { return atomic.AddInt64(&cs.miss, 1) }
-func (cs *cacheStats) ShouldLog(msg string) (func(), bool) {
+func (cs *cacheStats) shouldLog(msg string) (func(), int64, int64, bool) {
hit := atomic.LoadInt64(&cs.hit)
miss := atomic.LoadInt64(&cs.miss)
lookups := hit + miss
@@ -209,5 +213,5 @@ func (cs *cacheStats) ShouldLog(msg string) (func(), bool) {
"hitrate", str,
)
atomic.StoreInt32(&cs.flag, flag)
- }, atomic.LoadInt32(&cs.flag) != flag
+ }, hit, miss, atomic.LoadInt32(&cs.flag) != flag
}
diff --git a/muxdb/internal/trie/metrics.go b/muxdb/internal/trie/metrics.go
new file mode 100644
index 000000000..6db862df9
--- /dev/null
+++ b/muxdb/internal/trie/metrics.go
@@ -0,0 +1,12 @@
+// Copyright (c) 2024 The VeChainThor developers
+//
+// Distributed under the GNU Lesser General Public License v3.0 software license, see the accompanying
+// file LICENSE or
+
+package trie
+
+import (
+ "github.com/vechain/thor/v2/metrics"
+)
+
+var metricCacheHitMissGaugeVec = metrics.LazyLoadGaugeVec("cache_hit_miss_count", []string{"type", "event"})
From b0a3d7362e30834b525f07b316b6644d8e8199ae Mon Sep 17 00:00:00 2001
From: Darren Kelly <107671032+darrenvechain@users.noreply.github.com>
Date: Mon, 9 Dec 2024 12:49:42 +0000
Subject: [PATCH 14/15] chore(thor): update version (#912)
* chore(thor): update version
* chore(openapi): version
---
api/doc/thor.yaml | 2 +-
cmd/thor/VERSION | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/api/doc/thor.yaml b/api/doc/thor.yaml
index dcf0ae6b9..d8f6c45e8 100644
--- a/api/doc/thor.yaml
+++ b/api/doc/thor.yaml
@@ -12,7 +12,7 @@ info:
license:
name: LGPL 3.0
url: https://www.gnu.org/licenses/lgpl-3.0.en.html
- version: 2.1.4
+ version: 2.1.5
servers:
- url: /
description: Current Node
diff --git a/cmd/thor/VERSION b/cmd/thor/VERSION
index 7d2ed7c70..cd57a8b95 100644
--- a/cmd/thor/VERSION
+++ b/cmd/thor/VERSION
@@ -1 +1 @@
-2.1.4
+2.1.5
From c74bbf0159b08c048181beac82412e3af02af084 Mon Sep 17 00:00:00 2001
From: Delweng
Date: Mon, 9 Dec 2024 21:48:13 +0800
Subject: [PATCH 15/15] feat(api/debug): support debug trace without blockId
(#905)
* api/debug: support debug with txhash
Signed-off-by: jsvisa
api/debug: blockId should use tx's instead
Signed-off-by: jsvisa
fix tests
Signed-off-by: jsvisa
* debug: add test
Signed-off-by: jsvisa
* improve parseTarget
Signed-off-by: jsvisa
* update doc
Signed-off-by: jsvisa
* fix tests
Signed-off-by: jsvisa
---------
Signed-off-by: jsvisa
Co-authored-by: tony
---
api/debug/debug.go | 133 +++++++++++++++++++++++++---------------
api/debug/debug_test.go | 30 +++++++--
api/doc/thor.yaml | 9 +--
3 files changed, 116 insertions(+), 56 deletions(-)
diff --git a/api/debug/debug.go b/api/debug/debug.go
index 5ff54f1dc..497518982 100644
--- a/api/debug/debug.go
+++ b/api/debug/debug.go
@@ -75,22 +75,8 @@ func New(
}
}
-func (d *Debug) prepareClauseEnv(ctx context.Context, blockID thor.Bytes32, txIndex uint64, clauseIndex uint32) (*runtime.Runtime, *runtime.TransactionExecutor, thor.Bytes32, error) {
- block, err := d.repo.GetBlock(blockID)
- if err != nil {
- if d.repo.IsNotFound(err) {
- return nil, nil, thor.Bytes32{}, utils.Forbidden(errors.New("block not found"))
- }
- return nil, nil, thor.Bytes32{}, err
- }
- txs := block.Transactions()
- if txIndex >= uint64(len(txs)) {
- return nil, nil, thor.Bytes32{}, utils.Forbidden(errors.New("tx index out of range"))
- }
- txID := txs[txIndex].ID()
- if clauseIndex >= uint32(len(txs[txIndex].Clauses())) {
- return nil, nil, thor.Bytes32{}, utils.Forbidden(errors.New("clause index out of range"))
- }
+// prepareClauseEnv prepares the runtime environment for the specified clause.
+func (d *Debug) prepareClauseEnv(ctx context.Context, block *block.Block, txID thor.Bytes32, clauseIndex uint32) (*runtime.Runtime, *runtime.TransactionExecutor, thor.Bytes32, error) {
rt, err := consensus.New(
d.repo,
d.stater,
@@ -99,17 +85,29 @@ func (d *Debug) prepareClauseEnv(ctx context.Context, blockID thor.Bytes32, txIn
if err != nil {
return nil, nil, thor.Bytes32{}, err
}
- for i, tx := range txs {
- if uint64(i) > txIndex {
- break
+
+ var found bool
+ txs := block.Transactions()
+ for _, tx := range txs {
+ if txID == tx.ID() {
+ found = true
+ if clauseIndex >= uint32(len(tx.Clauses())) {
+ return nil, nil, thor.Bytes32{}, utils.Forbidden(errors.New("clause index out of range"))
+ }
}
+ }
+ if !found {
+ return nil, nil, thor.Bytes32{}, utils.Forbidden(errors.New("transaction not found"))
+ }
+
+ for _, tx := range block.Transactions() {
txExec, err := rt.PrepareTransaction(tx)
if err != nil {
return nil, nil, thor.Bytes32{}, err
}
clauseCounter := uint32(0)
for txExec.HasNextClause() {
- if txIndex == uint64(i) && clauseIndex == clauseCounter {
+ if tx.ID() == txID && clauseIndex == clauseCounter {
return rt, txExec, txID, nil
}
exec, _ := txExec.PrepareNext()
@@ -127,18 +125,27 @@ func (d *Debug) prepareClauseEnv(ctx context.Context, blockID thor.Bytes32, txIn
default:
}
}
+
+ // no env created, that means tx was reverted at an early clause
return nil, nil, thor.Bytes32{}, utils.Forbidden(errors.New("early reverted"))
}
// trace an existed clause
-func (d *Debug) traceClause(ctx context.Context, tracer tracers.Tracer, blockID thor.Bytes32, txIndex uint64, clauseIndex uint32) (interface{}, error) {
- rt, txExec, txID, err := d.prepareClauseEnv(ctx, blockID, txIndex, clauseIndex)
+func (d *Debug) traceClause(ctx context.Context, tracer tracers.Tracer, block *block.Block, txID thor.Bytes32, clauseIndex uint32) (interface{}, error) {
+ rt, txExec, txID, err := d.prepareClauseEnv(ctx, block, txID, clauseIndex)
if err != nil {
return nil, err
}
+ var txIndex uint64 = math.MaxUint64
+ for i, tx := range block.Transactions() {
+ if tx.ID() == txID {
+ txIndex = uint64(i)
+ break
+ }
+ }
tracer.SetContext(&tracers.Context{
- BlockID: blockID,
+ BlockID: block.Header().ID(),
BlockTime: rt.Context().Time,
TxID: txID,
TxIndex: txIndex,
@@ -178,11 +185,11 @@ func (d *Debug) handleTraceClause(w http.ResponseWriter, req *http.Request) erro
return utils.Forbidden(err)
}
- blockID, txIndex, clauseIndex, err := d.parseTarget(opt.Target)
+ block, txID, clauseIndex, err := d.parseTarget(opt.Target)
if err != nil {
return err
}
- res, err := d.traceClause(req.Context(), tracer, blockID, txIndex, clauseIndex)
+ res, err := d.traceClause(req.Context(), tracer, block, txID, clauseIndex)
if err != nil {
return err
}
@@ -291,8 +298,8 @@ func (d *Debug) traceCall(ctx context.Context, tracer tracers.Tracer, header *bl
return tracer.GetResult()
}
-func (d *Debug) debugStorage(ctx context.Context, contractAddress thor.Address, blockID thor.Bytes32, txIndex uint64, clauseIndex uint32, keyStart []byte, maxResult int) (*StorageRangeResult, error) {
- rt, _, _, err := d.prepareClauseEnv(ctx, blockID, txIndex, clauseIndex)
+func (d *Debug) debugStorage(ctx context.Context, contractAddress thor.Address, block *block.Block, txID thor.Bytes32, clauseIndex uint32, keyStart []byte, maxResult int) (*StorageRangeResult, error) {
+ rt, _, _, err := d.prepareClauseEnv(ctx, block, txID, clauseIndex)
if err != nil {
return nil, err
}
@@ -357,41 +364,71 @@ func (d *Debug) handleDebugStorage(w http.ResponseWriter, req *http.Request) err
return utils.WriteJSON(w, res)
}
-func (d *Debug) parseTarget(target string) (blockID thor.Bytes32, txIndex uint64, clauseIndex uint32, err error) {
+func (d *Debug) parseTarget(target string) (block *block.Block, txID thor.Bytes32, clauseIndex uint32, err error) {
+ // target can be `${blockID}/${txID|txIndex}/${clauseIndex}` or `${txID}/${clauseIndex}`
parts := strings.Split(target, "/")
- if len(parts) != 3 {
- return thor.Bytes32{}, 0, 0, utils.BadRequest(errors.New("target:" + target + " unsupported"))
+ if len(parts) != 3 && len(parts) != 2 {
+ return nil, thor.Bytes32{}, 0, utils.BadRequest(errors.New("target:" + target + " unsupported"))
}
- blockID, err = thor.ParseBytes32(parts[0])
- if err != nil {
- return thor.Bytes32{}, 0, 0, utils.BadRequest(errors.WithMessage(err, "target[0]"))
- }
- if len(parts[1]) == 64 || len(parts[1]) == 66 {
- txID, err := thor.ParseBytes32(parts[1])
+
+ if len(parts) == 2 {
+ txID, err = thor.ParseBytes32(parts[0])
if err != nil {
- return thor.Bytes32{}, 0, 0, utils.BadRequest(errors.WithMessage(err, "target[1]"))
+ return nil, thor.Bytes32{}, 0, utils.BadRequest(errors.WithMessage(err, "target([0]"))
}
-
- txMeta, err := d.repo.NewChain(blockID).GetTransactionMeta(txID)
+ txMeta, err := d.repo.NewBestChain().GetTransactionMeta(txID)
if err != nil {
if d.repo.IsNotFound(err) {
- return thor.Bytes32{}, 0, 0, utils.Forbidden(errors.New("transaction not found"))
+ return nil, thor.Bytes32{}, 0, utils.Forbidden(errors.New("transaction not found"))
}
- return thor.Bytes32{}, 0, 0, err
+ return nil, thor.Bytes32{}, 0, err
+ }
+ block, err = d.repo.GetBlock(txMeta.BlockID)
+ if err != nil {
+ return nil, thor.Bytes32{}, 0, err
}
- txIndex = txMeta.Index
} else {
- i, err := strconv.ParseUint(parts[1], 0, 0)
+ blockID, err := thor.ParseBytes32(parts[0])
+ if err != nil {
+ return nil, thor.Bytes32{}, 0, utils.BadRequest(errors.WithMessage(err, "target[0]"))
+ }
+ block, err = d.repo.GetBlock(blockID)
if err != nil {
- return thor.Bytes32{}, 0, 0, utils.BadRequest(errors.WithMessage(err, "target[1]"))
+ return nil, thor.Bytes32{}, 0, err
+ }
+ if len(parts[1]) == 64 || len(parts[1]) == 66 {
+ txID, err = thor.ParseBytes32(parts[1])
+ if err != nil {
+ return nil, thor.Bytes32{}, 0, utils.BadRequest(errors.WithMessage(err, "target[1]"))
+ }
+
+ var found bool
+ for _, tx := range block.Transactions() {
+ if tx.ID() == txID {
+ found = true
+ break
+ }
+ }
+ if !found {
+ return nil, thor.Bytes32{}, 0, utils.Forbidden(errors.New("transaction not found"))
+ }
+ } else {
+ i, err := strconv.ParseUint(parts[1], 0, 0)
+ if err != nil {
+ return nil, thor.Bytes32{}, 0, utils.BadRequest(errors.WithMessage(err, "target[1]"))
+ }
+ if i >= uint64(len(block.Transactions())) {
+ return nil, thor.Bytes32{}, 0, utils.Forbidden(errors.New("tx index out of range"))
+ }
+ txID = block.Transactions()[i].ID()
}
- txIndex = i
}
- i, err := strconv.ParseUint(parts[2], 0, 0)
+
+ i, err := strconv.ParseUint(parts[len(parts)-1], 0, 0)
if err != nil {
- return thor.Bytes32{}, 0, 0, utils.BadRequest(errors.WithMessage(err, "target[2]"))
+ return nil, thor.Bytes32{}, 0, utils.BadRequest(errors.WithMessage(err, fmt.Sprintf("target[%d]", len(parts)-1)))
} else if i > math.MaxUint32 {
- return thor.Bytes32{}, 0, 0, utils.BadRequest(errors.New("invalid target[2]"))
+ return nil, thor.Bytes32{}, 0, utils.BadRequest(fmt.Errorf("invalid target[%d]", len(parts)-1))
}
clauseIndex = uint32(i)
return
diff --git a/api/debug/debug_test.go b/api/debug/debug_test.go
index 1275a9030..d56718143 100644
--- a/api/debug/debug_test.go
+++ b/api/debug/debug_test.go
@@ -6,7 +6,6 @@
package debug
import (
- "context"
"encoding/json"
"fmt"
"math/big"
@@ -62,6 +61,7 @@ func TestDebug(t *testing.T) {
"testTraceClauseWithClauseIndexOutOfBound": testTraceClauseWithClauseIndexOutOfBound,
"testTraceClauseWithCustomTracer": testTraceClauseWithCustomTracer,
"testTraceClause": testTraceClause,
+ "testTraceClauseWithoutBlockID": testTraceClauseWithoutBlockID,
} {
t.Run(name, tt)
}
@@ -176,9 +176,11 @@ func testTraceClauseWithBadBlockID(t *testing.T) {
}
func testTraceClauseWithNonExistingBlockID(t *testing.T) {
- _, _, _, err := debug.prepareClauseEnv(context.Background(), datagen.RandomHash(), 1, 1)
-
- assert.Error(t, err)
+ traceClauseOption := &TraceClauseOption{
+ Name: "structLogger",
+ Target: fmt.Sprintf("%s/x/x", datagen.RandomHash()),
+ }
+ httpPostAndCheckResponseStatus(t, "/debug/tracers", traceClauseOption, 500)
}
func testTraceClauseWithBadTxID(t *testing.T) {
@@ -265,6 +267,26 @@ func testTraceClause(t *testing.T) {
assert.Equal(t, expectedExecutionResult, parsedExecutionRes)
}
+func testTraceClauseWithoutBlockID(t *testing.T) {
+ traceClauseOption := &TraceClauseOption{
+ Name: "structLogger",
+ Target: fmt.Sprintf("%s/1", transaction.ID()),
+ }
+ expectedExecutionResult := &logger.ExecutionResult{
+ Gas: 0,
+ Failed: false,
+ ReturnValue: "",
+ StructLogs: make([]logger.StructLogRes, 0),
+ }
+ res := httpPostAndCheckResponseStatus(t, "/debug/tracers", traceClauseOption, 200)
+
+ var parsedExecutionRes *logger.ExecutionResult
+ if err := json.Unmarshal([]byte(res), &parsedExecutionRes); err != nil {
+ t.Fatal(err)
+ }
+ assert.Equal(t, expectedExecutionResult, parsedExecutionRes)
+}
+
func testTraceClauseWithTxIndexOutOfBound(t *testing.T) {
traceClauseOption := &TraceClauseOption{
Name: "structLogger",
diff --git a/api/doc/thor.yaml b/api/doc/thor.yaml
index d8f6c45e8..702645411 100644
--- a/api/doc/thor.yaml
+++ b/api/doc/thor.yaml
@@ -2104,11 +2104,12 @@ components:
The unified path of the target to be traced. Currently, only the clause is supported.
Format:
- `blockID/(txIndex|txId)/clauseIndex`
+ `blockID/(txIndex|txId)/clauseIndex` or `txID/clauseIndex`
+
example: '0x010709463c1f0c9aa66a31182fb36d1977d99bfb6526bae0564a0eac4006c31a/0/0'
nullable: false
- pattern: '^0x[0-9a-fA-F]{64}\/(0x[0-9a-fA-F]{64}|\d+)\/[0-9]+$'
+ pattern: '^0x[0-9a-fA-F]{64}(\/(0x[0-9a-fA-F]{64}|\d+))?\/[0-9]+$'
example:
target: '0x010709463c1f0c9aa66a31182fb36d1977d99bfb6526bae0564a0eac4006c31a/0/0'
@@ -2174,9 +2175,9 @@ components:
The unified path of the transaction clause.
Format:
- `blockID/(txIndex|txId)/clauseIndex`
+ `blockID/(txIndex|txId)/clauseIndex` or `txID/clauseIndex`
nullable: false
- pattern: '^0x[0-9a-fA-F]{64}\/(0x[0-9a-fA-F]{64}|\d+)\/[0-9]+$'
+ pattern: '^0x[0-9a-fA-F]{64}(\/(0x[0-9a-fA-F]{64}|\d+))?\/[0-9]+$'
StorageRange:
type: object