-
Notifications
You must be signed in to change notification settings - Fork 3.5k
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
wip(interop) block dependency graph #10044
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,192 @@ | ||
package superchain | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"math/big" | ||
"sync" | ||
|
||
"github.com/ethereum-optimism/optimism/op-service/eth" | ||
"github.com/ethereum-optimism/optimism/op-service/sources" | ||
|
||
"github.com/ethereum/go-ethereum/common" | ||
"github.com/ethereum/go-ethereum/log" | ||
) | ||
|
||
type BlockSafetyLabel int | ||
|
||
const ( | ||
BlockUnsafe BlockSafetyLabel = iota - 1 | ||
BlockCrossUnsafe | ||
BlockSafe | ||
BlockFinalized | ||
) | ||
|
||
type blockDependency struct { | ||
chainId *big.Int | ||
blockNumber uint64 | ||
} | ||
|
||
type blockDependent struct { | ||
chainId *big.Int | ||
blockRef eth.L2BlockRef | ||
} | ||
|
||
type BlockDependencies struct { | ||
log log.Logger | ||
|
||
chains map[string]*sources.L2Client | ||
|
||
// We only track the heads and not any parent blocks previously added. The l2 | ||
// client source implements caching, simplfying memory management here. | ||
heads map[string]common.Hash | ||
|
||
// block -> unverified messages | ||
unverifiedExecutingMessages map[common.Hash][]Message | ||
|
||
// chain -> block -> dependencies | ||
// (1) Link between blocks with executing message to the blocks that should contain the initiating message | ||
// (2) The parent block is by default a dependency for a derived block | ||
dependencies map[string]map[common.Hash][]blockDependency | ||
|
||
// chain -> block number -> dependents | ||
// (1) Link between blocks with an initiating message that's been executed. | ||
// (2) Any derived block is by default a dependent for the parent. | ||
// | ||
// The block number is used here since the block containing the initiating | ||
// message may not have yet been observed when processing the executing message. | ||
dependents map[string]map[uint64][]blockDependent | ||
|
||
// Start with a global lock on the graph and avoid the optimization if it's not contentious | ||
mu sync.Mutex | ||
} | ||
|
||
func (deps *BlockDependencies) BlockSafety(ctx context.Context, chainId *big.Int, blockRef eth.L2BlockRef) (BlockSafetyLabel, error) { | ||
deps.mu.Lock() | ||
defer deps.mu.Unlock() | ||
|
||
if len(deps.unverifiedExecutingMessages[blockRef.Hash]) > 0 { | ||
return BlockUnsafe, nil | ||
} | ||
|
||
for _, blockDependency := range deps.dependencies[chainId.String()][blockRef.Hash] { | ||
// Since there are no unverified messages in this block, we can safely fetch the | ||
// right block using the block number as the initiating message in the remote | ||
// block was validated. | ||
// | ||
// We also know the remote block specified by number hasn't been reorg'd, | ||
// otherwise the invalidation would have cascaded to this block | ||
chain := deps.chains[blockDependency.chainId.String()] | ||
block, err := chain.L2BlockRefByNumber(ctx, blockDependency.blockNumber) | ||
if err != nil { | ||
return BlockUnsafe, err | ||
} | ||
|
||
dependencyBlockSafety, err := deps.BlockSafety(ctx, blockDependency.chainId, block) | ||
if err != nil { | ||
return BlockUnsafe, err | ||
} | ||
|
||
if dependencyBlockSafety == BlockUnsafe { | ||
return BlockUnsafe, nil | ||
} | ||
} | ||
|
||
// TODO: we need references to the safe head for every chain | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
return BlockCrossUnsafe, nil | ||
} | ||
|
||
func (deps *BlockDependencies) AddBlock(chainId *big.Int, blockRef eth.L2BlockRef) error { | ||
deps.mu.Lock() | ||
defer deps.mu.Unlock() | ||
|
||
deps.log.Debug("adding block", "chain_id", chainId, "hash", blockRef.Hash) | ||
|
||
chainIdStr := chainId.String() | ||
chain, ok := deps.chains[chainIdStr] | ||
if !ok { | ||
return fmt.Errorf("chain %d not present in configuration", chainId) | ||
} | ||
|
||
head := deps.heads[chainIdStr] | ||
if blockRef.ParentHash != head { | ||
return fmt.Errorf("block %s does not build on head %s", blockRef.Hash, head) | ||
} | ||
|
||
_, txs, err := chain.InfoAndTxsByHash(context.TODO(), blockRef.Hash) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
if err != nil { | ||
return fmt.Errorf("unable to query txs: %w", err) | ||
} | ||
|
||
// default edge with the parent block | ||
deps.dependents[chainIdStr][blockRef.Number-1] = append(deps.dependents[chainIdStr][blockRef.Number-1], blockDependent{chainId, blockRef}) | ||
deps.dependencies[chainIdStr][blockRef.Hash] = append(deps.dependencies[chainIdStr][blockRef.Hash], blockDependency{chainId, blockRef.Number - 1}) | ||
|
||
// add edges for present executing messages | ||
deps.heads[chainIdStr] = blockRef.Hash | ||
for _, tx := range txs { | ||
if IsInboxExecutingMessageTx(tx) { | ||
_, id, payload, err := ParseInboxExecuteMessageTxData(tx.Data()) | ||
if err != nil { | ||
// TODO: revisit bad txs to the inbox address | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
log.Warn("skipping inbox tx with bad tx data", "err", err) | ||
continue | ||
} | ||
|
||
dependent := blockDependent{id.ChainId, blockRef} | ||
dependency := blockDependency{id.ChainId, id.BlockNumber.Uint64()} | ||
|
||
// todo: de-dup edges | ||
deps.unverifiedExecutingMessages[blockRef.Hash] = append(deps.unverifiedExecutingMessages[blockRef.Hash], Message{id, payload}) | ||
deps.dependents[id.ChainId.String()][id.BlockNumber.Uint64()] = append(deps.dependents[id.ChainId.String()][id.BlockNumber.Uint64()], dependent) | ||
deps.dependencies[chainIdStr][blockRef.Hash] = append(deps.dependencies[chainIdStr][blockRef.Hash], dependency) | ||
} | ||
} | ||
|
||
// attempt resolution for this block & any existing dependents | ||
deps.resolveUnverifiedExecutingMessages(chainId, blockRef) | ||
for _, dependentBlock := range deps.dependents[chainIdStr][blockRef.Number] { | ||
deps.resolveUnverifiedExecutingMessages(dependentBlock.chainId, dependentBlock.blockRef) | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func (deps *BlockDependencies) resolveUnverifiedExecutingMessages(chainId *big.Int, blockRef eth.L2BlockRef) { | ||
deps.log.Debug("resolving unverified messages", "chain_id", chainId, "hash", blockRef.Hash) | ||
|
||
unverifiedMessages := deps.unverifiedExecutingMessages[blockRef.Hash] | ||
remainingUnverifiedMessages := make([]Message, 0, len(unverifiedMessages)) | ||
for _, msg := range unverifiedMessages { | ||
safety, err := MessageValidity(context.TODO(), msg.Id, msg.Payload, nil) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
if err != nil { | ||
if safety == MessageInvalid { | ||
deps.log.Error("invalidated msg", "id", msg.Id) | ||
deps.handleInvalidation(chainId, blockRef) | ||
} | ||
if safety == MessageUnknown { | ||
remainingUnverifiedMessages = append(remainingUnverifiedMessages, msg) | ||
} | ||
} | ||
// msg is valid (unsafe) and can be dropped. | ||
} | ||
|
||
deps.unverifiedExecutingMessages[blockRef.Hash] = remainingUnverifiedMessages | ||
} | ||
|
||
func (deps *BlockDependencies) handleInvalidation(chainId *big.Int, blockRef eth.L2BlockRef) { | ||
deps.log.Debug("block invalidation", "chain_id", chainId, "hash", blockRef.Hash) | ||
|
||
// new head is the parent | ||
chainIdStr := chainId.String() | ||
deps.heads[chainIdStr] = blockRef.ParentHash | ||
|
||
// first invalidate dependents (includes derived blocks) | ||
for _, dependentBlock := range deps.dependents[chainIdStr][blockRef.Number] { | ||
deps.handleInvalidation(dependentBlock.chainId, dependentBlock.blockRef) | ||
} | ||
|
||
// remove all edges from this block | ||
delete(deps.dependents[chainIdStr], blockRef.Number) | ||
delete(deps.dependencies[chainIdStr], blockRef.Hash) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please create a GitHub ticket for this TODO.
Ignore this finding from todos_require_linear.