Skip to content

Commit

Permalink
Initial commit to see the general idea of how Firehose tracer is inst…
Browse files Browse the repository at this point in the history
…antiated and configured

### Starter

**Important** This is a WIP PR that I push now so that you guys can see how it looks like and we then we can discuss concrete solutions to the questions I still have unanswered so far.

Here what I see as important decision points I took and open questions I have that will shape the future PR(s).

### Ethereum `core.BlockchainLogger` vs `x/evm/tracers#BlockchainLogger`

The standard Geth has `core.BlockchainLogger` which acts as a "live" tracer that traces the full block execution in addition to the EVM and State tracing.

However, the `core.BlockchainLogger` is made for Ethereum Block model in a way that is a bit too rigid to fit in Sei model, for example the `OnBlockStart` receives a `*types.Block` which is too hard to replicate in Sei model (if it was receiving `types.Header`, we could maybe have retrofit).

Furthermore, the `BlockchainLogger` direct methods (`OnBlockStart`, `OnBlockEnd`, etc.) are used in `core/blockchain.go`, execution point that is not present in Sei. So it was a bit moot to try to use `core.BlockchainLogger` in Sei directly due to required changes to the interface that would be needed.

We could have follow that route, e.g. changing the `core.BlockchainLogger` interface to fit Sei, but I judged it was better to limit the changes in `go-ethereum` and instead define our own version of `BlockchainLogger`.

For now I kept the same name and its semantics fits only for Ethereum transactions, so right now it's not a general Sei tracer.

I also prefix all methods of this interface with `OnSei...`, the reason for that being that it would enable one to implement both interface from a single struct, something I see could be a possibility for our `FirehoseTracer`.

Open questions:
- Right now the `x/evm/tracers#BlockchainLogger` is EVM aware only, should it become a general tracer for Sei with possibility to trace all type of transactions, including EVM? I think this discussion can be post-pone. First, a survey of Cosmos tracing world would be needed as we would probably want to follow current standard. Second, we could want to have this as a general Cosmos/Tendermint piece instead.

### Tracers Directory and Activation

Right now I enforced the instantiation of the `FirehoseTracer` straight in `app.go`. The Geth tracer PR introduces a registry were standard tracers are added to. The node operator can then activate the tracing by passing the flag `geth ... --vmtrace=<tracer-id>`, e.g. `geth ... --vmtrace=firehose`.

I plan to offer a similar experience in Sei, so I will add some kind of registry for the tracers + `func init()` registration. We could think of adding a `DebuggingTracer` that would print every tracing entry points.

If you could give me some hints on the names you would like to use, I'll prepare something.

### FirehoseTracer & Inclusion

The `FirehoseTracer` struct is our implementation of `x/evm/tracers@BlockchainLogger`. How it works is relatively simple. When a block start, we instantiate our Ethereum Block Model struct which is a Protobuf generated Go struct, definitions can be seen at https://buf.build/streamingfast/firehose-ethereum/docs/main:sf.ethereum.type.v2#sf.ethereum.type.v2.Block.

As the tracer is invoked (`CaptureTxStart`, `CaptureStart`, etc..) we decoded and accumulated all information in our block model. `CaptureTxStart` lead to a new active `TransactionTracer` which contains `Call[]` each call recording the various state changes (Log, Balance, Nonce, etc.)

On `OnBlockEnd`, the block is "completed", we serialize it to bytes then to base64 and we then emit in text format on `stdout` file descriptor the line `FIRE BLOCK <blocks's metadata> <final_block_ref> <bytes_base64_payload>`. Our `fireeth` binary manages the `seid start` process reading its `stdout` pipe and blocks progress through our code at this point.

I would like to have `FirehoseTracer` directly builtin in `sei-chain`. The main reason is to have an easy way for operators to operator Firehose node without having to download a forked binary that contains the Firehose interface. Morevover, the Firehose output format is relatively simply to parse and can be used in any language that supports Protobuf so external system could benefits from it.

### Parallelism, linearity

Right now the tracer in `app.go#ProcessBlock` is re-instantiated on each block as I noticed `ProcessBlock` is called within a goroutine which leads me to think that there is a possibility that 2 or more `ProcessBlock` could run concurrently.

Firehose needs to emit the block by locking the pipe to ensure that a single line is fully emitted before the next one, we wouldn't want concurrent write to `stdout` pipe.

I'm unsure where to ensure that linearity and exclusiveness is respected. For example if there is indeed 2 concurrents `ProcessBlock` running, where can I trigger the "emit" sequentially?

### Finality

Tony mentioned that `ProcessBlock` could be called from two code paths one final the other not final. Firehose handles forks without a problem but needs finality information about the block, we need to know which parent(s) block is final relative to the current block.

The idea is in pseudo-code something like this:

```
ProcessBlock(block Block):
  OnBlockStart(block, findFirstFinalBlockStartingFrom(block))
  ...
```

Chain(s) usually have some kind of deterministic way to determine the final block. In case of Sei with its instant finality and in regards to `ProcessBlock`, we have multiple avenue:

- Maybe the final/non-final execution is true only for a miner node and is not relevant for a full node that is simply syncing with the network. If it's the case in Sei, then I would simply use the current block as being final (and add a sanity check if the block is non-final and a tracer is active).
- The tracer is only called if the execution of `ProcessBlock` is final, would need to see where I can get that information.
- We determine what is the right version of `findFirstFinalBlockStartingFrom(block)` for Sei chain and I use that

### `RunPrecompiledContract` and `RunAndCalculateGas`

In `RunPrecompiledContract` there is `RunAndCalculateGas` which is an early return. In `RunPrecompiledContract` method we track gas via `OnGasChange` which means we need to also instrument `RunAndCalculateGas` which is an interface.

What is the correct way to instrument this, this is billing some gas if I understand it right. However `RunAndCalculateGas` is an interface so there is multiple implementation, they all must be aware of the tracer.

### Sei Address Association

Sei has address association from sei <=> EVM based on the fact the the underlying private key is secp256k1 but the public keys is mapped differently. Is this tracked in some EVM contract/pre-compiled or is it kept in Sei kvdb? Should we track this somehow?

### State Snapshot

Our standard Ethereum implementation records the genesis block with the genesis balances, code and storage for the various genesis account. Now it appears that when Sei upgrade to EVM support, the current `usei` balance is carried over to EVM.

I see the Firehose tracer as starting from the very block that will enable EVM meaning state prior that point will not be known to Firehose user. This is a problem for advanced technology that are built on the fact that the tracer keeps sees the full state of the node including genesis data.

One possibility I see here is that `x/evm/types/BlockchainLogger` get an extra `OnEVMGenesisState`. Now, the exact interface of how to query the state would need to be defined.

I sense of the amount of data we talk about will drive the decision of such API. Indeed, if you tell me there is currently 10M Sei users, maybe holding it full in memory is not the best choice.

Would need to know also if there is other state that caries over. I saw some ERC20 likes stuff and I think some mapping could be in the plan so I imagine here also the initial state would be kept in Sei.

### Other Changes

Outside of the `RunAndCalculateGas`, in this vein of tracking any state changes happening around the EVM block execution, do you guys see other things that are particular to Sei but fiddles someone with the EVM known state?
  • Loading branch information
maoueh committed Feb 10, 2024
1 parent 5eb635f commit 0517c62
Show file tree
Hide file tree
Showing 14 changed files with 5,857 additions and 23 deletions.
60 changes: 56 additions & 4 deletions app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,17 @@ import (
"encoding/json"
"fmt"
"io"
"math/big"
"os"
"path/filepath"
"strings"
"sync"
"time"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
ethtypes "github.com/ethereum/go-ethereum/core/types"

"github.com/sei-protocol/sei-chain/app/antedecorators"
"github.com/sei-protocol/sei-chain/evmrpc"
"github.com/sei-protocol/sei-chain/precompiles"
Expand Down Expand Up @@ -119,6 +124,7 @@ import (
"github.com/sei-protocol/sei-chain/x/evm"
evmante "github.com/sei-protocol/sei-chain/x/evm/ante"
evmkeeper "github.com/sei-protocol/sei-chain/x/evm/keeper"
evmtracers "github.com/sei-protocol/sei-chain/x/evm/tracers"
evmtypes "github.com/sei-protocol/sei-chain/x/evm/types"
"github.com/spf13/cast"
abci "github.com/tendermint/tendermint/abci/types"
Expand Down Expand Up @@ -1374,11 +1380,16 @@ func (app *App) BuildDependenciesAndRunTxs(ctx sdk.Context, txs [][]byte, typedT
return app.ProcessBlockSynchronous(ctx, txs, typedTxs, absoluteTxIndices), ctx
}

func (app *App) ProcessBlock(ctx sdk.Context, txs [][]byte, req BlockProcessRequest, lastCommit abci.CommitInfo) ([]abci.Event, []*abci.ExecTxResult, abci.ResponseEndBlock, error) {
func (app *App) ProcessBlock(ctx sdk.Context, txs [][]byte, req BlockProcessRequest, lastCommit abci.CommitInfo) (events []abci.Event, txResults []*abci.ExecTxResult, endBlockResp abci.ResponseEndBlock, err error) {
goCtx := app.decorateContextWithDexMemState(ctx.Context())
ctx = ctx.WithContext(goCtx)

events := []abci.Event{}
tracer := evmtracers.NewFirehoseLogger()
tracer.OnSeiBlockchainInit(app.EvmKeeper.GetChainConfig(ctx).EthereumConfig(app.EvmKeeper.ChainID(ctx)))

ctx = evmtracers.SetCtxBlockchainLogger(ctx, tracer)

events = []abci.Event{}
beginBlockReq := abci.RequestBeginBlock{
Hash: req.GetHash(),
ByzantineValidators: utils.Map(req.GetByzantineValidators(), func(mis abci.Misbehavior) abci.Evidence {
Expand All @@ -1403,9 +1414,15 @@ func (app *App) ProcessBlock(ctx sdk.Context, txs [][]byte, req BlockProcessRequ
beginBlockResp := app.BeginBlock(ctx, beginBlockReq)
events = append(events, beginBlockResp.Events...)

txResults := make([]*abci.ExecTxResult, len(txs))
txResults = make([]*abci.ExecTxResult, len(txs))
typedTxs := []sdk.Tx{}

header := ctx.BlockHeader()
tracer.OnSeiBlockStart(req.GetHash(), uint64(header.Size()), TmBlockHeaderToEVM(ctx, header, &app.EvmKeeper))
defer func() {
tracer.OnSeiBlockEnd(err)
}()

for i, tx := range txs {
typedTx, err := app.txDecoder(tx)
// get txkey from tx
Expand Down Expand Up @@ -1448,7 +1465,7 @@ func (app *App) ProcessBlock(ctx sdk.Context, txs [][]byte, req BlockProcessRequ
lazyWriteEvents := app.BankKeeper.WriteDeferredBalances(ctx)
events = append(events, lazyWriteEvents...)

endBlockResp := app.EndBlock(ctx, abci.RequestEndBlock{
endBlockResp = app.EndBlock(ctx, abci.RequestEndBlock{
Height: req.GetHeight(),
})

Expand Down Expand Up @@ -1727,3 +1744,38 @@ func init() {
// override max wasm size to 2MB
wasmtypes.MaxWasmSize = 2 * 1024 * 1024
}

func TmBlockHeaderToEVM(
ctx sdk.Context,
block tmproto.Header,
k *evmkeeper.Keeper,
) (header *ethtypes.Header) {
number := big.NewInt(block.Height)
lastHash := common.BytesToHash(block.LastBlockId.Hash)
appHash := common.BytesToHash(block.AppHash)
txHash := common.BytesToHash(block.DataHash)
resultHash := common.BytesToHash(block.LastResultsHash)
miner := common.BytesToAddress(block.ProposerAddress)
gasLimit, gasWanted := uint64(0), uint64(0)

header = &ethtypes.Header{
Number: number,
ParentHash: lastHash,
Nonce: ethtypes.BlockNonce{}, // inapplicable to Sei
MixDigest: common.Hash{}, // inapplicable to Sei
UncleHash: ethtypes.EmptyUncleHash, // inapplicable to Sei
Bloom: k.GetBlockBloom(ctx, block.Height),
Root: appHash,
Coinbase: miner,
Difficulty: big.NewInt(0), // inapplicable to Sei
Extra: hexutil.Bytes{}, // inapplicable to Sei
GasLimit: gasLimit,
GasUsed: gasWanted,
Time: uint64(block.Time.Unix()),
TxHash: txHash,
ReceiptHash: resultHash,
BaseFee: k.GetBaseFeePerGas(ctx).RoundInt().BigInt(),
}

return
}
6 changes: 6 additions & 0 deletions buf.gen.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
version: v1
plugins:
- plugin: buf.build/protocolbuffers/go:v1.31.0
out: pb
opt: paths=source_relative

2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,7 @@ replace (
github.com/cosmos/cosmos-sdk => github.com/sei-protocol/sei-cosmos v0.2.73-evm-rebase-8
github.com/cosmos/iavl => github.com/sei-protocol/sei-iavl v0.1.9
github.com/cosmos/ibc-go/v3 => github.com/sei-protocol/sei-ibc-go/v3 v3.3.0
github.com/ethereum/go-ethereum => github.com/sei-protocol/go-ethereum v1.13.5-sei-7
github.com/ethereum/go-ethereum => github.com/streamingfast/go-ethereum v1.13.6-0.20240209215202-04d3f64a96f9
github.com/gogo/protobuf => github.com/regen-network/protobuf v1.3.3-alpha.regen.1
github.com/sei-protocol/sei-db => github.com/sei-protocol/sei-db v0.0.30
// Latest goleveldb is broken, we have to stick to this version
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1342,8 +1342,6 @@ github.com/seccomp/libseccomp-golang v0.9.2-0.20220502022130-f33da4d89646/go.mod
github.com/securego/gosec/v2 v2.11.0 h1:+PDkpzR41OI2jrw1q6AdXZCbsNGNGT7pQjal0H0cArI=
github.com/securego/gosec/v2 v2.11.0/go.mod h1:SX8bptShuG8reGC0XS09+a4H2BoWSJi+fscA+Pulbpo=
github.com/segmentio/fasthash v1.0.3/go.mod h1:waKX8l2N8yckOgmSsXJi7x1ZfdKZ4x7KRMzBtS3oedY=
github.com/sei-protocol/go-ethereum v1.13.5-sei-7 h1:axbdah9mqg6RooVbJjaqmvd8tz0IFsvEVWc613pizLw=
github.com/sei-protocol/go-ethereum v1.13.5-sei-7/go.mod h1:kcRZmuzRn1lVejiFNTz4l4W7imnpq1bDAnuKS/RyhbQ=
github.com/sei-protocol/goutils v0.0.2 h1:Bfa7Sv+4CVLNM20QcpvGb81B8C5HkQC/kW1CQpIbXDA=
github.com/sei-protocol/goutils v0.0.2/go.mod h1:iYE2DuJfEnM+APPehr2gOUXfuLuPsVxorcDO+Tzq9q8=
github.com/sei-protocol/sei-cosmos v0.2.73-evm-rebase-8 h1:ueyLkyzlvuInVIPKvd4FLqe/N8+wZSRtfh5C0RMi7rQ=
Expand Down Expand Up @@ -1429,6 +1427,8 @@ github.com/stbenjam/no-sprintf-host-port v0.1.1/go.mod h1:TLhvtIvONRzdmkFiio4O8L
github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw=
github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI=
github.com/streamingfast/go-ethereum v1.13.6-0.20240209215202-04d3f64a96f9 h1:CaT0eNJX4P1sB4pDYx1oihxuV2hM71aiMJbs2SikRqo=
github.com/streamingfast/go-ethereum v1.13.6-0.20240209215202-04d3f64a96f9/go.mod h1:kcRZmuzRn1lVejiFNTz4l4W7imnpq1bDAnuKS/RyhbQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
Expand Down
27 changes: 27 additions & 0 deletions pb/sf/ethereum/type/v2/type.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package pbeth

import (
"encoding/hex"
"math/big"
"time"
)

var b0 = big.NewInt(0)

func (b *Block) PreviousID() string {
return hex.EncodeToString(b.Header.ParentHash)
}

func (b *Block) Time() time.Time {
return b.Header.Timestamp.AsTime()
}

func (m *BigInt) Native() *big.Int {
if m == nil {
return b0
}

z := new(big.Int)
z.SetBytes(m.Bytes)
return z
}
Loading

0 comments on commit 0517c62

Please sign in to comment.