Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat(octane/evmengine): support blob txs #2498

Merged
merged 1 commit into from
Nov 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions e2e/app/perturb.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,15 +128,15 @@ func perturbNode(ctx context.Context, node *e2e.Node, perturbation e2e.Perturbat

case e2e.PerturbationUpgrade:
log.Info(ctx, "Perturb node: upgrade", "from", node.Version, "to", testnet.UpgradeVersion)
if err := docker.ExecCompose(ctx, testnet.Dir, "stop", name); err != nil {
if err := docker.ExecCompose(ctx, testnet.Dir, "down", name); err != nil {
return nil, errors.Wrap(err, "stop service")
}

if err := docker.ReplaceUpgradeImage(testnet.Dir, name); err != nil {
return nil, errors.Wrap(err, "upgrade service")
}

if err := docker.ExecCompose(ctx, testnet.Dir, "start", name); err != nil {
if err := docker.ExecCompose(ctx, testnet.Dir, "up", "-d", name); err != nil {
return nil, errors.Wrap(err, "start service")
}

Expand Down
130 changes: 130 additions & 0 deletions e2e/test/geth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,27 @@ package e2e_test

import (
"context"
"crypto/sha256"
"math/big"
"testing"
"time"

"github.com/omni-network/omni/e2e/app/geth"
"github.com/omni-network/omni/lib/anvil"
"github.com/omni-network/omni/lib/errors"
"github.com/omni-network/omni/lib/ethclient"
"github.com/omni-network/omni/lib/netconf"
"github.com/omni-network/omni/lib/umath"

"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/consensus/misc/eip4844"
ethtypes "github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/crypto/kzg4844"
"github.com/ethereum/go-ethereum/params"

"github.com/holiman/uint256"
"github.com/stretchr/testify/require"
)

Expand All @@ -32,3 +45,120 @@ func TestGethConfig(t *testing.T) {
require.NotEqual(t, common.Hash{}, *block.BeaconRoot())
})
}

func TestBlobTx(t *testing.T) {
t.Parallel()
testOmniEVM(t, func(t *testing.T, client ethclient.Client) {
t.Helper()
err := sendBlobTx(context.Background(), client)
require.NoError(t, err)
})
}

func sendBlobTx(ctx context.Context, client ethclient.Client) error {
privKey := anvil.DevPrivateKey1()
addr := crypto.PubkeyToAddress(privKey.PublicKey)

// Create a blob tx
blobTx, err := makeUnsignedBlobTx(ctx, client, addr)
if err != nil {
return err
}
tx, err := ethtypes.SignNewTx(privKey,
ethtypes.NewCancunSigner(umath.NewBigInt(netconf.Devnet.Static().OmniExecutionChainID)),
blobTx)
if err != nil {
return err
}

ctx, cancel := context.WithTimeout(ctx, time.Second*10)
defer cancel()

// Submit it and confirm status
err = client.SendTransaction(ctx, tx)
if err != nil {
return err
}
rec, err := bind.WaitMined(ctx, client, tx)
if err != nil {
return err
} else if ethtypes.ReceiptStatusSuccessful != rec.Status {
return errors.New("receipt status not successful")
}

return nil
}

// makeUnsignedBlobTx is a utility method to construct a random blob transaction
// without signing it.
// Reference: github.com/ethereum/go-ethereum@v1.14.11/core/txpool/blobpool/blobpool_test.go:184.
func makeUnsignedBlobTx(ctx context.Context, client ethclient.Client, from common.Address) (*ethtypes.BlobTx, error) {
nonce, err := client.NonceAt(ctx, from, nil)
if err != nil {
return nil, errors.Wrap(err, "nonce")
}

tipCap, baseFee, blobFee, err := estimateGasPrice(ctx, client)
if err != nil {
return nil, errors.Wrap(err, "estimate gas price")
}

emptyBlob := new(kzg4844.Blob)
emptyBlobCommit, err := kzg4844.BlobToCommitment(emptyBlob)
if err != nil {
return nil, errors.Wrap(err, "blob commitment")
}
emptyBlobProof, err := kzg4844.ComputeBlobProof(emptyBlob, emptyBlobCommit)
if err != nil {
return nil, errors.Wrap(err, "blob proof")
}
emptyBlobVHash := kzg4844.CalcBlobHashV1(sha256.New(), &emptyBlobCommit)

return &ethtypes.BlobTx{
ChainID: uint256.NewInt(netconf.Devnet.Static().OmniExecutionChainID),
Nonce: nonce,
GasTipCap: uint256.NewInt(tipCap.Uint64()),
GasFeeCap: uint256.NewInt(baseFee.Uint64()),
BlobFeeCap: uint256.NewInt(blobFee.Uint64()),
Gas: 21000,
BlobHashes: []common.Hash{emptyBlobVHash},
Sidecar: &ethtypes.BlobTxSidecar{
Blobs: []kzg4844.Blob{*emptyBlob},
Commitments: []kzg4844.Commitment{emptyBlobCommit},
Proofs: []kzg4844.Proof{emptyBlobProof},
},
}, nil
}

func estimateGasPrice(ctx context.Context, backend ethclient.Client) (*big.Int, *big.Int, *big.Int, error) {
tip, err := backend.SuggestGasTipCap(ctx)
if err != nil {
return nil, nil, nil, err
}

head, err := backend.HeaderByNumber(ctx, nil)
if err != nil {
return nil, nil, nil, err
}
if head.BaseFee == nil {
return nil, nil, nil, errors.New("txmgr does not support pre-london blocks that do not have a base fee")
}

baseFee := head.BaseFee
minBase := big.NewInt(params.GWei) // Minimum base fee is 1 GWei.
if baseFee.Cmp(minBase) < 0 {
baseFee = minBase
}

var blobFee *big.Int
if head.ExcessBlobGas != nil {
blobFee = eip4844.CalcBlobFee(*head.ExcessBlobGas)
}

// The tip must be at most the base fee.
if tip.Cmp(baseFee) > 0 {
tip = baseFee
}

return tip, baseFee, blobFee, nil
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ require (
github.com/hdevalence/ed25519consensus v0.1.0 // indirect
github.com/holiman/billy v0.0.0-20240216141850-2abb0c79d3c4 // indirect
github.com/holiman/bloomfilter/v2 v2.0.3 // indirect
github.com/holiman/uint256 v1.3.1 // indirect
github.com/holiman/uint256 v1.3.1
github.com/huandu/skiplist v1.2.0 // indirect
github.com/huin/goupnp v1.3.0 // indirect
github.com/iancoleman/strcase v0.3.0 // indirect
Expand Down
9 changes: 9 additions & 0 deletions lib/cast/cast.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@ func Array65[A any](slice []A) ([65]A, error) {
return [65]A{}, errors.New("slice length not 65", "len", len(slice))
}

// Array48 casts a slice to an array of length 48.
func Array48[A any](slice []A) ([48]A, error) {
if len(slice) == 48 {
return [48]A(slice), nil
}

return [48]A{}, errors.New("slice length not 48", "len", len(slice))
}

// Must32 casts a slice to an array of length 32.
func Must32[A any](slice []A) [32]A {
arr, err := Array32(slice)
Expand Down
1 change: 1 addition & 0 deletions lib/ethclient/enginemock.go
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,7 @@ func (m *engineMock) GetPayloadV3(ctx context.Context, payloadID engine.PayloadI

return &engine.ExecutionPayloadEnvelope{
ExecutionPayload: &args.params,
BlobsBundle: &engine.BlobsBundleV1{}, // Empty blobs
}, nil
}

Expand Down
7 changes: 7 additions & 0 deletions octane/evmengine/keeper/abci.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,12 @@ func (k *Keeper) PrepareProposal(ctx sdk.Context, req *abci.RequestPreparePropos
return nil, errors.Wrap(err, "encode")
}

// Convert blobs bundle.
blobCommitments := unwrapHexBytes(payloadResp.BlobsBundle.Commitments)
if _, err := blobHashes(blobCommitments); err != nil { // Sanity check blobs are valid.
return nil, errors.Wrap(err, "invalid blobs [BUG]")
}

// First, collect all vote extension msgs from the vote provider.
voteMsgs, err := k.voteProvider.PrepareVotes(ctx, req.LocalLastCommit, uint64(req.Height-1))
if err != nil {
Expand All @@ -137,6 +143,7 @@ func (k *Keeper) PrepareProposal(ctx sdk.Context, req *abci.RequestPreparePropos
Authority: authtypes.NewModuleAddress(types.ModuleName).String(),
ExecutionPayload: payloadData,
PrevPayloadEvents: evmEvents,
BlobCommitments: blobCommitments,
}

// Combine all the votes messages and the payload message into a single transaction.
Expand Down
85 changes: 83 additions & 2 deletions octane/evmengine/keeper/abci_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"github.com/ethereum/go-ethereum"
eengine "github.com/ethereum/go-ethereum/beacon/engine"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/trie"

Expand Down Expand Up @@ -200,7 +201,7 @@ func TestKeeper_PrepareProposal(t *testing.T) {

for _, msg := range tx.GetMsgs() {
if _, ok := msg.(*etypes.MsgExecutionPayload); ok {
assertExecutablePayload(t, msg, ts.Unix(), nextBlock.Hash(), frp, uint64(req.Height))
assertExecutablePayload(t, msg, ts.Unix(), nextBlock.Hash(), frp, uint64(req.Height), 0)
}
}
})
Expand Down Expand Up @@ -249,7 +250,7 @@ func TestKeeper_PrepareProposal(t *testing.T) {
// assert that the message is an executable payload
for _, msg := range tx.GetMsgs() {
if _, ok := msg.(*etypes.MsgExecutionPayload); ok {
assertExecutablePayload(t, msg, req.Time.Unix(), headHash, frp, head.GetBlockHeight()+1)
assertExecutablePayload(t, msg, req.Time.Unix(), headHash, frp, head.GetBlockHeight()+1, 0)
}
if msgDelegate, ok := msg.(*stypes.MsgDelegate); ok {
require.Equal(t, msgDelegate.Amount, sdk.NewInt64Coin("stake", 100))
Expand All @@ -259,6 +260,78 @@ func TestKeeper_PrepareProposal(t *testing.T) {
// make sure all msg.Delegate are present
require.Equal(t, 1, actualDelCount)
})

t.Run("TestBlobCommitments", func(t *testing.T) {
t.Parallel()
// setup dependencies
ctx, storeService := setupCtxStore(t, nil)
cdc := getCodec(t)
txConfig := authtx.NewTxConfig(cdc, nil)

commitment1 := tutil.RandomBytes(48)
commitment2 := tutil.RandomBytes(48)

mockEngine, err := newMockEngineAPI(0)
require.NoError(t, err)
mockEngine.getPayloadV3Func = func(ctx context.Context, payloadID eengine.PayloadID) (*eengine.ExecutionPayloadEnvelope, error) {
resp, err := mockEngine.mock.GetPayloadV3(ctx, payloadID)
if err != nil {
return nil, err
}

resp.BlobsBundle.Commitments = []hexutil.Bytes{
commitment1,
commitment2,
}

return resp, nil
}

ap := mockAddressProvider{
address: common.BytesToAddress([]byte("test")),
}
frp := newRandomFeeRecipientProvider()
keeper, err := NewKeeper(cdc, storeService, &mockEngine, txConfig, ap, frp, mockLogProvider{})
require.NoError(t, err)
keeper.SetVoteProvider(mockVEProvider{})
populateGenesisHead(ctx, t, keeper)

// Get the parent block we will build on top of
head, err := keeper.getExecutionHead(ctx)
require.NoError(t, err)
headHash, err := head.Hash()
require.NoError(t, err)

req := &abci.RequestPrepareProposal{
Txs: nil,
Height: int64(2),
Time: time.Now(),
MaxTxBytes: cmttypes.MaxBlockSizeBytes,
}

resp, err := keeper.PrepareProposal(withRandomErrs(t, ctx), req)
tutil.RequireNoError(t, err)
require.NotNil(t, resp)

// decode the txn and get the messages
tx, err := txConfig.TxDecoder()(resp.Txs[0])
require.NoError(t, err)

// assert that the message is an executable payload
var found bool
for _, msg := range tx.GetMsgs() {
payload, ok := msg.(*etypes.MsgExecutionPayload)
if !ok {
continue
}

found = true
expected := [][]byte{commitment1, commitment2}
require.EqualValues(t, expected, payload.BlobCommitments)
assertExecutablePayload(t, msg, req.Time.Unix(), headHash, frp, head.GetBlockHeight()+1, len(expected))
}
require.True(t, found)
})
}

func TestOptimistic(t *testing.T) {
Expand Down Expand Up @@ -330,6 +403,7 @@ func assertExecutablePayload(
blockHash common.Hash,
frp etypes.FeeRecipientProvider,
height uint64,
blobs int,
) {
t.Helper()
executionPayload, ok := msg.(*etypes.MsgExecutionPayload)
Expand All @@ -348,6 +422,8 @@ func assertExecutablePayload(
require.Len(t, executionPayload.PrevPayloadEvents, 1)
evmLog := executionPayload.PrevPayloadEvents[0]
require.Equal(t, evmLog.Address, zeroAddr.Bytes())

require.Len(t, executionPayload.BlobCommitments, blobs)
}

func ctxWithAppHash(t *testing.T, appHash common.Hash) context.Context {
Expand Down Expand Up @@ -409,6 +485,7 @@ type mockEngineAPI struct {
headerByTypeFunc func(context.Context, ethclient.HeadType) (*types.Header, error)
forkchoiceUpdatedV3Func func(context.Context, eengine.ForkchoiceStateV1, *eengine.PayloadAttributes) (eengine.ForkChoiceResponse, error)
newPayloadV3Func func(context.Context, eengine.ExecutableData, []common.Hash, *common.Hash) (eengine.PayloadStatusV1, error)
getPayloadV3Func func(ctx context.Context, payloadID eengine.PayloadID) (*eengine.ExecutionPayloadEnvelope, error)
}

// newMockEngineAPI returns a new mock engine API with a fuzzer and a mock engine client.
Expand Down Expand Up @@ -533,6 +610,10 @@ func (m *mockEngineAPI) ForkchoiceUpdatedV3(ctx context.Context, update eengine.
}

func (m *mockEngineAPI) GetPayloadV3(ctx context.Context, payloadID eengine.PayloadID) (*eengine.ExecutionPayloadEnvelope, error) {
if m.getPayloadV3Func != nil {
return m.getPayloadV3Func(ctx, payloadID)
}

return m.mock.GetPayloadV3(ctx, payloadID)
}

Expand Down
2 changes: 1 addition & 1 deletion octane/evmengine/keeper/evmengine.proto
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@ message ExecutionHead {
uint64 block_height = 3; // Execution block height.
bytes block_hash = 4; // Execution block hash.
uint64 block_time = 5; // Execution block time.
}
}
Loading
Loading