Skip to content
Closed
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: 4 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
version: "2"
run:
modules-download-mode: readonly
build-tags:
- evm
- e2e
- docker
linters:
enable:
- errorlint
Expand Down
2 changes: 1 addition & 1 deletion apps/testapp/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ replace (
require (
github.com/ipfs/go-datastore v0.8.2
github.com/rollkit/rollkit v0.0.0-00010101000000-000000000000
github.com/rollkit/rollkit/core v0.0.0-20250312114929-104787ba1a4c
github.com/rollkit/rollkit/da v0.0.0-00010101000000-000000000000
github.com/rollkit/rollkit/sequencers/single v0.0.0-00010101000000-000000000000
github.com/spf13/cobra v1.9.1
Expand Down Expand Up @@ -141,7 +142,6 @@ require (
github.com/quic-go/quic-go v0.50.1 // indirect
github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66 // indirect
github.com/raulk/go-watchdog v1.3.0 // indirect
github.com/rollkit/rollkit/core v0.0.0-20250312114929-104787ba1a4c // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
Expand Down
7 changes: 7 additions & 0 deletions apps/testapp/kv/kvexecutor.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

ds "github.com/ipfs/go-datastore"
"github.com/ipfs/go-datastore/query"
"github.com/rollkit/rollkit/core/execution"
"github.com/rollkit/rollkit/pkg/store"
)

Expand Down Expand Up @@ -243,6 +244,12 @@ func (k *KVExecutor) SetFinal(ctx context.Context, blockHeight uint64) error {
return k.db.Put(ctx, ds.NewKey("/finalizedHeight"), []byte(fmt.Sprintf("%d", blockHeight)))
}

// GetExecutionMode returns the execution mode for this executor.
// KVExecutor uses delayed execution mode.
func (k *KVExecutor) GetExecutionMode() execution.ExecutionMode {
return execution.ExecutionModeDelayed
}

// InjectTx adds a transaction to the mempool channel.
// Uses a non-blocking send to avoid blocking the caller if the channel is full.
func (k *KVExecutor) InjectTx(tx []byte) {
Expand Down
7 changes: 6 additions & 1 deletion block/da_includer.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"context"
"encoding/binary"
"fmt"
"strings"

coreda "github.com/rollkit/rollkit/core/da"
storepkg "github.com/rollkit/rollkit/pkg/store"
Expand Down Expand Up @@ -36,7 +37,7 @@
}
// Both header and data are DA-included, so we can advance the height
if err := m.incrementDAIncludedHeight(ctx); err != nil {
errCh <- fmt.Errorf("error while incrementing DA included height: %w", err)
errCh <- fmt.Errorf("while incrementing DA included height: %w", err)

Check warning on line 40 in block/da_includer.go

View check run for this annotation

Codecov / codecov/patch

block/da_includer.go#L40

Added line #L40 was not covered by tests
return
}

Expand All @@ -57,6 +58,10 @@
m.logger.Debug("setting final height: ", newHeight)
err := m.exec.SetFinal(ctx, newHeight)
if err != nil {
if strings.Contains(err.Error(), "not yet indexed") {
m.logger.Debug("engine not ready yet, will retry later", "height", newHeight)
return nil
}

Check warning on line 64 in block/da_includer.go

View check run for this annotation

Codecov / codecov/patch

block/da_includer.go#L62-L64

Added lines #L62 - L64 were not covered by tests
m.logger.Error("failed to set final height: ", newHeight, "error: ", err)
return err
}
Expand Down
122 changes: 96 additions & 26 deletions block/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,8 @@
metrics *Metrics

exec coreexecutor.Executor
// executionMode caches the execution mode from the executor
executionMode coreexecutor.ExecutionMode

// daIncludedHeight is rollkit height at which all blocks have been included
// in the DA
Expand Down Expand Up @@ -351,6 +353,8 @@
daH := atomic.Uint64{}
daH.Store(s.DAHeight)

executionMode := exec.GetExecutionMode()

m := &Manager{
signer: signer,
config: config,
Expand Down Expand Up @@ -380,6 +384,7 @@
metrics: seqMetrics,
sequencer: sequencer,
exec: exec,
executionMode: executionMode,
da: da,
gasPrice: gasPrice,
gasMultiplier: gasMultiplier,
Expand Down Expand Up @@ -800,22 +805,26 @@
}

// // Verify that the header's timestamp is strictly greater than the last block's time
// headerTime := header.Time()
// if header.Height() > 1 && lastState.LastBlockTime.After(headerTime) {
// return fmt.Errorf("block time must be strictly increasing: got %v, last block time was %v",
// headerTime.UnixNano(), lastState.LastBlockTime)
// }

// // Validate that the header's AppHash matches the lastState's AppHash
// // Note: Assumes deferred execution
// if !bytes.Equal(header.AppHash, lastState.AppHash) {
// return fmt.Errorf("app hash mismatch: expected %x, got %x", lastState.AppHash, header.AppHash)
// }
headerTime := header.Time()
if header.Height() > 1 && lastState.LastBlockTime.After(headerTime) {
return fmt.Errorf("block time must be strictly increasing: got %v, last block time was %v",
headerTime.UnixNano(), lastState.LastBlockTime)
}

// Validate AppHash based on execution mode
if m.executionMode == coreexecutor.ExecutionModeDelayed {
// In delayed mode, AppHash should match the last state's AppHash
if !bytes.Equal(header.AppHash, lastState.AppHash) {
return fmt.Errorf("appHash mismatch in delayed mode: expected %x, got %x", lastState.AppHash, header.AppHash)
}
}
// Note: For immediate mode, we can't validate AppHash against lastState because
// it represents the state after executing the current block's transactions

return nil
}

func (m *Manager) execCreateBlock(_ context.Context, height uint64, lastSignature *types.Signature, lastHeaderHash types.Hash, _ types.State, batchData *BatchData) (*types.SignedHeader, *types.Data, error) {
func (m *Manager) execCreateBlock(ctx context.Context, height uint64, lastSignature *types.Signature, lastHeaderHash types.Hash, lastState types.State, batchData *BatchData) (*types.SignedHeader, *types.Data, error) {
// Use when batchData is set to data IDs from the DA layer
// batchDataIDs := convertBatchDataToBytes(batchData.Data)

Expand All @@ -840,21 +849,36 @@
// Determine if this is an empty block
isEmpty := batchData.Batch == nil || len(batchData.Transactions) == 0

// Determine AppHash based on execution mode
var appHash []byte
if m.executionMode == coreexecutor.ExecutionModeImmediate {

// Execute transactions
newStateRoot, _, err := m.exec.ExecuteTxs(ctx, batchData.Transactions, height, batchData.Time, lastState.AppHash)
if err != nil {
return nil, nil, fmt.Errorf("failed to execute transactions for immediate mode: %w", err)
}
appHash = newStateRoot

Check warning on line 861 in block/manager.go

View check run for this annotation

Codecov / codecov/patch

block/manager.go#L855-L861

Added lines #L855 - L861 were not covered by tests
} else {
// For delayed execution, use the app hash from last state
appHash = lastState.AppHash
}

header := &types.SignedHeader{
Header: types.Header{
Version: types.Version{
Block: m.lastState.Version.Block,
App: m.lastState.Version.App,
Block: lastState.Version.Block,
App: lastState.Version.App,
},
BaseHeader: types.BaseHeader{
ChainID: m.lastState.ChainID,
ChainID: lastState.ChainID,
Height: height,
Time: uint64(batchData.UnixNano()), //nolint:gosec // why is time unix? (tac0turtle)
},
LastHeaderHash: lastHeaderHash,
// DataHash is set at the end of the function
// DataHash is set below
ConsensusHash: make(types.Hash, 32),
AppHash: m.lastState.AppHash,
AppHash: appHash,
ProposerAddress: m.genesis.ProposerAddress,
},
Signature: *lastSignature,
Expand All @@ -869,7 +893,7 @@
Txs: make(types.Txs, 0), // Start with empty transaction list
}

// Only add transactions if this is not an empty block
// Set DataHash
if !isEmpty {
blockData.Txs = make(types.Txs, len(batchData.Transactions))
for i := range batchData.Transactions {
Expand All @@ -884,15 +908,61 @@
}

func (m *Manager) execApplyBlock(ctx context.Context, lastState types.State, header *types.SignedHeader, data *types.Data) (types.State, error) {
rawTxs := make([][]byte, len(data.Txs))
for i := range data.Txs {
rawTxs[i] = data.Txs[i]
}
var newStateRoot []byte

// Check if we need to execute transactions
if m.executionMode == coreexecutor.ExecutionModeImmediate {
// For immediate mode, the aggregator already executed during block creation
// But syncing nodes need to execute to verify the AppHash
// Check if we're the block creator by verifying we signed this block
isBlockCreator := false
if m.signer != nil {
myAddress, err := m.signer.GetAddress()
if err == nil && bytes.Equal(header.ProposerAddress, myAddress) {
// Also verify this is the current height we're producing
currentHeight, err := m.store.Height(ctx)
if err == nil && header.Height() == currentHeight+1 {
isBlockCreator = true
}

Check warning on line 926 in block/manager.go

View check run for this annotation

Codecov / codecov/patch

block/manager.go#L915-L926

Added lines #L915 - L926 were not covered by tests
}
}

ctx = context.WithValue(ctx, types.SignedHeaderContextKey, header)
newStateRoot, _, err := m.exec.ExecuteTxs(ctx, rawTxs, header.Height(), header.Time(), lastState.AppHash)
if err != nil {
return types.State{}, fmt.Errorf("failed to execute transactions: %w", err)
if isBlockCreator {
// We created this block, so we already executed transactions in execCreateBlock
// Just use the AppHash from the header
newStateRoot = header.AppHash
} else {
// We're syncing this block, need to execute to verify AppHash
rawTxs := make([][]byte, len(data.Txs))
for i := range data.Txs {
rawTxs[i] = data.Txs[i]
}

Check warning on line 939 in block/manager.go

View check run for this annotation

Codecov / codecov/patch

block/manager.go#L930-L939

Added lines #L930 - L939 were not covered by tests

ctx = context.WithValue(ctx, types.SignedHeaderContextKey, header)
computedStateRoot, _, err := m.exec.ExecuteTxs(ctx, rawTxs, header.Height(), header.Time(), lastState.AppHash)
if err != nil {
return types.State{}, fmt.Errorf("failed to execute transactions: %w", err)
}

Check warning on line 945 in block/manager.go

View check run for this annotation

Codecov / codecov/patch

block/manager.go#L941-L945

Added lines #L941 - L945 were not covered by tests

// Verify the AppHash matches
if !bytes.Equal(header.AppHash, computedStateRoot) {
return types.State{}, fmt.Errorf("AppHash mismatch in immediate mode: expected %x, got %x", computedStateRoot, header.AppHash)
}
newStateRoot = computedStateRoot

Check warning on line 951 in block/manager.go

View check run for this annotation

Codecov / codecov/patch

block/manager.go#L948-L951

Added lines #L948 - L951 were not covered by tests
}
Comment on lines +915 to +952
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Complex branching logic needs careful testing.

The logic for determining whether to execute transactions in immediate mode is complex with multiple conditions. Consider adding unit tests to cover all branches, especially:

  • Block creator path (trusting header's AppHash)
  • Syncing node path (verifying AppHash)
  • Error cases in address/height retrieval

Based on the static analysis, these execution paths lack test coverage. Would you like me to help generate comprehensive unit tests for the immediate execution mode?


🏁 Script executed:

#!/bin/bash
# Search for unit tests covering the immediate execution mode branching in block/manager.go

# 1. Tests mentioning "immediate mode"
rg -n "immediate mode" --glob "*_test.go"

# 2. Tests invoking ExecuteTxs
rg -n "ExecuteTxs" --glob "*_test.go"

# 3. Tests referencing signer.GetAddress
rg -n "GetAddress" --glob "*_test.go"

# 4. Tests referencing store.Height
rg -n "Height(" --glob "*_test.go"

Length of output: 5308


Add unit tests covering immediate‐mode branching in block/manager.go

The complex logic in block/manager.go (lines 924–961) isn’t directly exercised by any existing tests. Please add unit tests around the following paths to ensure full coverage:

• Block creator path (m.signer.GetAddress succeeds, store.Height returns currentHeight, header.Height()==currentHeight+1 → newStateRoot = header.AppHash; ExecuteTxs must not be called)
• Syncing path (any of the signer/height checks fail → ExecuteTxs executes all Txs, matches header.AppHash)
• Error cases:
– signer.GetAddress returns an error
– store.Height returns an error
– exec.ExecuteTxs returns an error (propagate failure)
– AppHash mismatch between header and computedStateRoot

Locations needing tests:

  • block/manager.go:924–961
🧰 Tools
🪛 GitHub Check: codecov/patch

[warning] 924-935: block/manager.go#L924-L935
Added lines #L924 - L935 were not covered by tests


[warning] 939-948: block/manager.go#L939-L948
Added lines #L939 - L948 were not covered by tests


[warning] 950-954: block/manager.go#L950-L954
Added lines #L950 - L954 were not covered by tests


[warning] 957-960: block/manager.go#L957-L960
Added lines #L957 - L960 were not covered by tests

🤖 Prompt for AI Agents
In block/manager.go between lines 924 and 961, add unit tests to cover all
branches of the immediate-mode logic. Create tests for the block creator path
where m.signer.GetAddress and store.Height succeed and header.Height equals
currentHeight+1, ensuring newStateRoot is set to header.AppHash and ExecuteTxs
is not called. Add tests for the syncing path where any signer or height check
fails, verifying ExecuteTxs runs all transactions and the computed AppHash
matches header.AppHash. Also, include tests for error cases where
signer.GetAddress returns an error, store.Height returns an error,
exec.ExecuteTxs returns an error (which should propagate), and where there is an
AppHash mismatch between header and computedStateRoot.

} else {
// For delayed mode, always execute transactions
rawTxs := make([][]byte, len(data.Txs))
for i := range data.Txs {
rawTxs[i] = data.Txs[i]
}
Comment on lines +955 to +958
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This block of code to convert types.Txs to [][]byte is duplicated from lines 945-948 in this same function, and is also present in execCreateBlock. Consider extracting this logic into a helper function to improve maintainability and reduce code duplication.


ctx = context.WithValue(ctx, types.SignedHeaderContextKey, header)
var err error
newStateRoot, _, err = m.exec.ExecuteTxs(ctx, rawTxs, header.Height(), header.Time(), lastState.AppHash)
if err != nil {
return types.State{}, fmt.Errorf("failed to execute transactions: %w", err)
}
}

s, err := lastState.NextState(header, newStateRoot)
Expand Down
42 changes: 20 additions & 22 deletions block/manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -575,28 +575,26 @@ func TestManager_execValidate(t *testing.T) {
require.NoError(err)
})

// TODO: https://github.com/rollkit/rollkit/issues/2250

// t.Run("non-monotonic block time with height > 1", func(t *testing.T) {
// state, header, data, privKey := makeValid()
// state.LastBlockTime = time.Now().Add(time.Minute)
// state.LastBlockHeight = 1
// header.BaseHeader.Height = state.LastBlockHeight + 1
// data.Metadata.Height = state.LastBlockHeight + 1
// signer, err := noopsigner.NewNoopSigner(privKey)
// require.NoError(err)
// header.Signature, err = types.GetSignature(header.Header, signer)
// require.NoError(err)
// err = m.execValidate(state, header, data)
// require.ErrorContains(err, "block time must be strictly increasing")
// })

// t.Run("app hash mismatch", func(t *testing.T) {
// state, header, data, _ := makeValid()
// state.AppHash = []byte("different")
// err := m.execValidate(state, header, data)
// require.ErrorContains(err, "app hash mismatch")
// })
t.Run("non-monotonic block time with height > 1", func(t *testing.T) {
state, header, data, privKey := makeValid()
state.LastBlockTime = time.Now().Add(time.Minute)
state.LastBlockHeight = 1
header.BaseHeader.Height = state.LastBlockHeight + 1
data.Metadata.Height = state.LastBlockHeight + 1
signer, err := noopsigner.NewNoopSigner(privKey)
require.NoError(err)
header.Signature, err = types.GetSignature(header.Header, signer)
require.NoError(err)
err = m.execValidate(state, header, data)
require.ErrorContains(err, "block time must be strictly increasing")
})

t.Run("app hash mismatch", func(t *testing.T) {
state, header, data, _ := makeValid()
state.AppHash = []byte("different")
err := m.execValidate(state, header, data)
require.ErrorContains(err, "appHash mismatch in delayed mode")
})
}

// TestGetterMethods tests simple getter methods for the Manager
Expand Down
5 changes: 5 additions & 0 deletions block/publish_block_p2p_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

coreexecution "github.com/rollkit/rollkit/core/execution"
coresequencer "github.com/rollkit/rollkit/core/sequencer"
"github.com/rollkit/rollkit/pkg/config"
genesispkg "github.com/rollkit/rollkit/pkg/genesis"
Expand Down Expand Up @@ -241,6 +242,10 @@ func (m mockExecutor) SetFinal(ctx context.Context, blockHeight uint64) error {
return nil
}

func (m mockExecutor) GetExecutionMode() coreexecution.ExecutionMode {
return coreexecution.ExecutionModeDelayed
}

var rnd = rand.New(rand.NewSource(1)) //nolint:gosec // test code only

func bytesN(n int) []byte {
Expand Down
2 changes: 1 addition & 1 deletion block/submitter.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ func submitToDA[T any](

switch res.Code {
case coreda.StatusSuccess:
m.logger.Info(fmt.Sprintf("successfully submitted %s to DA layer", itemType), "gasPrice", gasPrice, "count", res.SubmittedCount)
m.logger.Info(fmt.Sprintf("successfully submitted %s to DA layer,", itemType), "gasPrice ", gasPrice, "count", res.SubmittedCount)
if res.SubmittedCount == uint64(remLen) {
submittedAll = true
}
Expand Down
2 changes: 1 addition & 1 deletion block/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ func (m *Manager) handleEmptyDataHash(ctx context.Context, header *types.Header)
if headerHeight > 1 {
_, lastData, err := m.store.GetBlockData(ctx, headerHeight-1)
if err != nil {
m.logger.Debug("previous block not applied yet", "current height", headerHeight, "previous height", headerHeight-1, "error", err)
m.logger.Debug("previous block not applied yet, current height: ", headerHeight, "previous height: ", headerHeight-1, "error: ", err)
}
if lastData != nil {
lastDataHash = lastData.Hash()
Expand Down
6 changes: 6 additions & 0 deletions core/execution/dummy.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,12 @@
return fmt.Errorf("cannot set finalized block at height %d", blockHeight)
}

// GetExecutionMode returns the execution mode for this executor.
// DummyExecutor uses delayed execution mode.
func (e *DummyExecutor) GetExecutionMode() ExecutionMode {
return ExecutionModeDelayed

Check warning on line 97 in core/execution/dummy.go

View check run for this annotation

Codecov / codecov/patch

core/execution/dummy.go#L96-L97

Added lines #L96 - L97 were not covered by tests
}

func (e *DummyExecutor) removeExecutedTxs(txs [][]byte) {
e.injectedTxs = slices.DeleteFunc(e.injectedTxs, func(tx []byte) bool {
return slices.ContainsFunc(txs, func(t []byte) bool { return bytes.Equal(tx, t) })
Expand Down
Loading
Loading