1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
package cross
import (
"errors"
"fmt"
"strings"
"github.com/ethereum-optimism/optimism/op-service/eth"
"github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types"
)
var (
ErrCycle = errors.New("cycle detected")
ErrExecMsgHasInvalidIndex = errors.New("executing message has invalid log index")
ErrExecMsgUnknownChain = errors.New("executing message references unknown chain")
)
// CycleCheckDeps is an interface for checking cyclical dependencies between logs.
type CycleCheckDeps interface {
// OpenBlock returns log data for the requested block, to be used for cycle checking.
OpenBlock(chainID types.ChainID, blockNum uint64) (block eth.BlockRef, logCount uint32, execMsgs map[uint32]*types.ExecutingMessage, err error)
}
// node represents a log entry in the dependency graph.
// It could be an initiating message, executing message, both, or neither.
// It is uniquely identified by chain index and the log index within its parent block.
type node struct {
chainIndex types.ChainIndex
logIndex uint32
}
// graph is a directed graph of message dependencies.
// It is represented as an adjacency list with in-degree counts to be friendly to cycle checking.
type graph struct {
inDegree0 map[node]struct{}
inDegreeNon0 map[node]uint32
outgoingEdges map[node][]node
}
// addEdge adds a directed edge from -> to in the graph.
func (g *graph) addEdge(from, to node) {
// Remove the target from inDegree0 if it's there
delete(g.inDegree0, to)
// Add or increment the target's in-degree count
g.inDegreeNon0[to] += 1
// Add the outgoing edge
g.outgoingEdges[from] = append(g.outgoingEdges[from], to)
}
// HazardCycleChecks checks for cyclical dependencies between logs at the given timestamp.
// Here the timestamp invariant alone does not ensure ordering of messages.
//
// We perform this check in 3 steps:
// - Gather all logs across all hazard blocks at the given timestamp.
// - Build the logs into a directed graph of dependencies between logs.
// - Check the graph for cycles.
//
// The edges of the graph are determined by:
// - For all logs except the first in a block, there is an edge from the previous log.
// - For all executing messages, there is an edge from the initiating message.
//
// The edges between sequential logs ensure the graph is well-connected and free of any
// disjoint subgraphs that would make cycle checking more difficult.
//
// The cycle check is performed by executing Kahn's topological sort algorithm which
// succeeds if and only if a graph is acyclic.
//
// Returns nil if no cycles are found or ErrCycle if a cycle is detected.
func HazardCycleChecks(d CycleCheckDeps, inTimestamp uint64, hazards map[types.ChainIndex]types.BlockSeal) error {
g, err := buildGraph(d, inTimestamp, hazards)
if err != nil {
return err
}
return checkGraph(g)
}
// gatherLogs collects all log counts and executing messages across all hazard blocks.
// Returns:
// - map of chain index to its log count
// - map of chain index to map of log index to executing message (nil if doesn't exist or ignored)
func gatherLogs(d CycleCheckDeps, inTimestamp uint64, hazards map[types.ChainIndex]types.BlockSeal) (
map[types.ChainIndex]uint32,
map[types.ChainIndex]map[uint32]*types.ExecutingMessage,
error,
) {
logCounts := make(map[types.ChainIndex]uint32)
execMsgs := make(map[types.ChainIndex]map[uint32]*types.ExecutingMessage)
for hazardChainIndex, hazardBlock := range hazards {
// TODO(#11105): translate chain index to chain ID
hazardChainID := types.ChainIDFromUInt64(uint64(hazardChainIndex))
bl, logCount, msgs, err := d.OpenBlock(hazardChainID, hazardBlock.Number)
if err != nil {
return nil, nil, fmt.Errorf("failed to open block: %w", err)
}
if !blockSealMatchesRef(hazardBlock, bl) {
return nil, nil, fmt.Errorf("tried to open block %s of chain %s, but got different block %s than expected, use a reorg lock for consistency", hazardBlock, hazardChainID, bl)
}
// Validate executing message indices
for logIdx := range msgs {
if logIdx >= logCount {
return nil, nil, fmt.Errorf("%w: log index %d >= log count %d", ErrExecMsgHasInvalidIndex, logIdx, logCount)
}
}
// Store log count and in-timestamp executing messages
logCounts[hazardChainIndex] = logCount
if len(msgs) > 0 {
if _, exists := execMsgs[hazardChainIndex]; !exists {
execMsgs[hazardChainIndex] = make(map[uint32]*types.ExecutingMessage)
}
}
for logIdx, msg := range msgs {
if msg.Timestamp == inTimestamp {
execMsgs[hazardChainIndex][logIdx] = msg
}
}
}
return logCounts, execMsgs, nil
}
// buildGraph constructs a dependency graph from the hazard blocks.
func buildGraph(d CycleCheckDeps, inTimestamp uint64, hazards map[types.ChainIndex]types.BlockSeal) (*graph, error) {
g := &graph{
inDegree0: make(map[node]struct{}),
inDegreeNon0: make(map[node]uint32),
outgoingEdges: make(map[node][]node),
}
logCounts, execMsgs, err := gatherLogs(d, inTimestamp, hazards)
if err != nil {
return nil, err
}
// Add nodes for each log in the block, and add edges between sequential logs
for hazardChainIndex, logCount := range logCounts {
for i := uint32(0); i < logCount; i++ {
k := node{
chainIndex: hazardChainIndex,
logIndex: i,
}
if i == 0 {
// First log in block has no dependencies
g.inDegree0[k] = struct{}{}
} else {
// Add edge: prev log <> current log
prevKey := node{
chainIndex: hazardChainIndex,
logIndex: i - 1,
}
g.addEdge(prevKey, k)
}
}
}
// Add edges for executing messages to their initiating messages
for hazardChainIndex, msgs := range execMsgs {
for execLogIdx, m := range msgs {
// Error if the chain is unknown
if _, ok := hazards[m.Chain]; !ok {
return nil, ErrExecMsgUnknownChain
}
// Check if we care about the init message
initChainMsgs, ok := execMsgs[m.Chain]
if !ok {
continue
}
if _, ok := initChainMsgs[m.LogIdx]; !ok {
continue
}
// Check if the init message exists
if logCount, ok := logCounts[m.Chain]; !ok || m.LogIdx >= logCount {
return nil, fmt.Errorf("%w: initiating message log index out of bounds", types.ErrConflict)
}
initKey := node{
chainIndex: m.Chain,
logIndex: m.LogIdx,
}
execKey := node{
chainIndex: hazardChainIndex,
logIndex: execLogIdx,
}
// Disallow self-referencing messages
// This should not be possible since the executing message contains the hash of the initiating message.
if initKey == execKey {
return nil, fmt.Errorf("%w: self referencial message", types.ErrConflict)
}
// Add the edge
g.addEdge(initKey, execKey)
}
}
return g, nil
}
// checkGraph uses Kahn's topological sort algorithm to check for cycles in the graph.
//
// Returns:
// - nil for acyclic graphs.
// - ErrCycle for cyclic graphs.
//
// Algorithm:
// 1. for each node with in-degree 0 (i.e. no dependencies), add it to the result, remove it from the work.
// 2. along with removing, remove the outgoing edges
// 3. if there is no node left with in-degree 0, then there is a cycle
func checkGraph(g *graph) error {
for {
// Process all nodes that have no incoming edges
for k := range g.inDegree0 {
// Remove all outgoing edges from this node
for _, out := range g.outgoingEdges[k] {
g.inDegreeNon0[out] -= 1
if g.inDegreeNon0[out] == 0 {
delete(g.inDegreeNon0, out)
g.inDegree0[out] = struct{}{}
}
}
delete(g.outgoingEdges, k)
delete(g.inDegree0, k)
}
// If there are new nodes with in-degree 0 then process them
if len(g.inDegree0) > 0 {
continue
}
// We're done processing so check for remaining nodes
if len(g.inDegreeNon0) == 0 {
// Done, without cycles!
return nil
}
// Some nodes left; there must be a cycle.
return ErrCycle
}
}
func blockSealMatchesRef(seal types.BlockSeal, ref eth.BlockRef) bool {
return seal.Number == ref.Number && seal.Hash == ref.Hash
}
// GenerateMermaidDiagram creates a Mermaid flowchart diagram from the graph data for debugging.
func GenerateMermaidDiagram(g *graph) string {
var sb strings.Builder
sb.WriteString("flowchart TD\n")
// Helper function to get a unique ID for each node
getNodeID := func(k node) string {
return fmt.Sprintf("N%d_%d", k.chainIndex, k.logIndex)
}
// Helper function to get a label for each node
getNodeLabel := func(k node) string {
return fmt.Sprintf("C%d:L%d", k.chainIndex, k.logIndex)
}
// Function to add a node to the diagram
addNode := func(k node, inDegree uint32) {
nodeID := getNodeID(k)
nodeLabel := getNodeLabel(k)
var shape string
if inDegree == 0 {
shape = "((%s))"
} else {
shape = "[%s]"
}
sb.WriteString(fmt.Sprintf(" %s"+shape+"\n", nodeID, nodeLabel))
}
// Add all nodes
for k := range g.inDegree0 {
addNode(k, 0)
}
for k, inDegree := range g.inDegreeNon0 {
addNode(k, inDegree)
}
// Add all edges
for from, tos := range g.outgoingEdges {
fromID := getNodeID(from)
for _, to := range tos {
toID := getNodeID(to)
sb.WriteString(fmt.Sprintf(" %s --> %s\n", fromID, toID))
}
}
// Add a legend
sb.WriteString(" subgraph Legend\n")
sb.WriteString(" L1((In-Degree 0))\n")
sb.WriteString(" L2[In-Degree > 0]\n")
sb.WriteString(" end\n")
return sb.String()
}