-
Notifications
You must be signed in to change notification settings - Fork 40
/
call_sequence_execution.go
166 lines (144 loc) · 7.08 KB
/
call_sequence_execution.go
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
package calls
import (
"fmt"
"github.com/crytic/medusa/chain"
)
// ExecuteCallSequenceFetchElementFunc describes a function that is called to obtain the next call sequence element to
// execute. It is given the current call index in the sequence.
// Returns the call sequence element to execute, or an error if one occurs. If the call sequence element is nil,
// it indicates the end of the sequence and execution breaks.
type ExecuteCallSequenceFetchElementFunc func(index int) (*CallSequenceElement, error)
// ExecuteCallSequenceExecutionCheckFunc describes a function that is called after each call is executed in a
// sequence. It is given the currently executed call sequence to this point.
// Returns a boolean indicating if the sequence execution should break, or an error if one occurs.
type ExecuteCallSequenceExecutionCheckFunc func(currentExecutedSequence CallSequence) (bool, error)
// ExecuteCallSequenceIteratively executes a CallSequence upon a provided chain iteratively. It ensures calls are
// included in blocks which adhere to the CallSequence properties (such as delays) as much as possible.
// A "fetch next call" function is provided to fetch the next element to execute.
// A "post element executed check" function is provided to check whether execution should stop after each element is
// executed.
// Returns the call sequence which was executed and an error if one occurs.
func ExecuteCallSequenceIteratively(chain *chain.TestChain, fetchElementFunc ExecuteCallSequenceFetchElementFunc, executionCheckFunc ExecuteCallSequenceExecutionCheckFunc) (CallSequence, error) {
// If there is no fetch element function provided, throw an error
if fetchElementFunc == nil {
return nil, fmt.Errorf("could not execute call sequence on chain as the 'fetch element function' provided was nil")
}
// Create a call sequence to track all elements executed throughout this operation.
var callSequenceExecuted CallSequence
// Create a variable to track if the post-execution check operation requested we break execution.
execCheckFuncRequestedBreak := false
// Loop through each sequence element in our sequence we'll want to execute.
for i := 0; true; i++ {
// Call our "fetch next call" function and obtain our next call sequence element.
callSequenceElement, err := fetchElementFunc(i)
if err != nil {
return callSequenceExecuted, err
}
// If we are at the end of our sequence, break out of our execution loop.
if callSequenceElement == nil {
break
}
// We try to add the transaction with our call more than once. If the pending block is too full, we may hit a
// block gas limit, which we handle by committing the pending block without this tx, and creating a new pending
// block that is empty to try adding this tx there instead.
// If we encounter an error on an empty block, we throw the error as there is nothing more we can do.
for {
// If we have a pending block, but we intend to delay this call from the last, we commit that block.
if chain.PendingBlock() != nil && callSequenceElement.BlockNumberDelay > 0 {
err := chain.PendingBlockCommit()
if err != nil {
return callSequenceExecuted, err
}
}
// If we have no pending block to add a tx containing our call to, we must create one.
if chain.PendingBlock() == nil {
// The minimum step between blocks must be 1 in block number and timestamp, so we ensure this is the
// case.
numberDelay := callSequenceElement.BlockNumberDelay
timeDelay := callSequenceElement.BlockTimestampDelay
if numberDelay == 0 {
numberDelay = 1
}
if timeDelay == 0 {
timeDelay = 1
}
// Each timestamp/block number must be unique as well, so we cannot jump more block numbers than time.
if numberDelay > timeDelay {
numberDelay = timeDelay
}
_, err := chain.PendingBlockCreateWithParameters(chain.Head().Header.Number.Uint64()+numberDelay, chain.Head().Header.Time+timeDelay, nil)
if err != nil {
return callSequenceExecuted, err
}
}
// Try to add our transaction to this block.
err = chain.PendingBlockAddTx(callSequenceElement.Call.ToCoreMessage())
if err != nil {
// If we encountered a block gas limit error, this tx is too expensive to fit in this block.
// If there are other transactions in the block, this makes sense. The block is "full".
// In that case, we commit the pending block without this tx, and create a new pending block to add
// our tx to, and iterate to try and add it again.
// TODO: This should also check the condition that this is a block gas error specifically. For now, we
// simply assume it is and try processing in an empty block (if that fails, that error will be
// returned).
if len(chain.PendingBlock().Messages) > 0 {
err := chain.PendingBlockCommit()
if err != nil {
return callSequenceExecuted, err
}
continue
}
// If there are no transactions in our block, and we failed to add this one, return the error
return callSequenceExecuted, err
}
// Update our chain reference for this element.
callSequenceElement.ChainReference = &CallSequenceElementChainReference{
Block: chain.PendingBlock(),
TransactionIndex: len(chain.PendingBlock().Messages) - 1,
}
// Add to our executed call sequence
callSequenceExecuted = append(callSequenceExecuted, callSequenceElement)
// We added our call to the block as a transaction. Call our step function with the update and check
// if it returned an error.
if executionCheckFunc != nil {
execCheckFuncRequestedBreak, err = executionCheckFunc(callSequenceExecuted)
if err != nil {
return callSequenceExecuted, err
}
// If post-execution check requested we break execution, break out of our "retry loop"
if execCheckFuncRequestedBreak {
break
}
}
// We didn't encounter an error, so we were successful in adding this transaction. Break out of this
// inner "retry loop" and move onto processing the next element in the outer loop.
break
}
// If post-execution check requested we break execution, break out of our "execute next call sequence loop"
if execCheckFuncRequestedBreak {
break
}
}
// Commit the last pending block.
if chain.PendingBlock() != nil {
err := chain.PendingBlockCommit()
if err != nil {
return callSequenceExecuted, err
}
}
return callSequenceExecuted, nil
}
// ExecuteCallSequence executes a provided CallSequence on the provided chain.
// It returns the slice of the call sequence which was tested, and an error if one occurred.
// If no error occurred, it can be expected that the returned call sequence contains all elements originally provided.
func ExecuteCallSequence(chain *chain.TestChain, callSequence CallSequence) (CallSequence, error) {
// Execute our sequence with a simple fetch operation provided to obtain each element.
fetchElementFunc := func(currentIndex int) (*CallSequenceElement, error) {
if currentIndex < len(callSequence) {
return callSequence[currentIndex], nil
}
return nil, nil
}
// Execute our provided call sequence iteratively.
return ExecuteCallSequenceIteratively(chain, fetchElementFunc, nil)
}