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
package sequencer
import (
"bytes"
"encoding/binary"
"fmt"
"io"
"math"
l2types "github.com/ethereum-optimism/optimism/l2geth/core/types"
l2rlp "github.com/ethereum-optimism/optimism/l2geth/rlp"
)
var byteOrder = binary.BigEndian
// BatchContext denotes a range of transactions that belong the same batch. It
// is used to compress shared fields that would otherwise be repeated for each
// transaction.
type BatchContext struct {
// NumSequencedTxs specifies the number of sequencer txs included in
// the batch.
NumSequencedTxs uint64 `json:"num_sequenced_txs"`
// NumSubsequentQueueTxs specifies the number of queued txs included in
// the batch
NumSubsequentQueueTxs uint64 `json:"num_subsequent_queue_txs"`
// Timestamp is the L1 timestamp of the batch.
Timestamp uint64 `json:"timestamp"`
// BlockNumber is the L1 BlockNumber of the batch.
BlockNumber uint64 `json:"block_number"`
}
// Write encodes the BatchContext into a 16-byte stream using the following
// encoding:
// - num_sequenced_txs: 3 bytes
// - num_subsequent_queue_txs: 3 bytes
// - timestamp: 5 bytes
// - block_number: 5 bytes
func (c *BatchContext) Write(w *bytes.Buffer) {
writeUint64(w, c.NumSequencedTxs, 3)
writeUint64(w, c.NumSubsequentQueueTxs, 3)
writeUint64(w, c.Timestamp, 5)
writeUint64(w, c.BlockNumber, 5)
}
// Read decodes the BatchContext from the passed reader. If fewer than 16-bytes
// remain, an error is returned. Otherwise the first 16-bytes will be read using
// the expected encoding:
// - num_sequenced_txs: 3 bytes
// - num_subsequent_queue_txs: 3 bytes
// - timestamp: 5 bytes
// - block_number: 5 bytes
func (c *BatchContext) Read(r io.Reader) error {
if err := readUint64(r, &c.NumSequencedTxs, 3); err != nil {
return err
}
if err := readUint64(r, &c.NumSubsequentQueueTxs, 3); err != nil {
return err
}
if err := readUint64(r, &c.Timestamp, 5); err != nil {
return err
}
return readUint64(r, &c.BlockNumber, 5)
}
// AppendSequencerBatchParams holds the raw data required to submit a batch of
// L2 txs to L1 CTC contract. Rather than encoding the objects using the
// standard ABI encoding, a custom encoding is and provided in the call data to
// optimize for gas fees, since batch submission of L2 txs is a primary cost
// driver.
type AppendSequencerBatchParams struct {
// ShouldStartAtElement specifies the intended starting sequence number
// of the provided transaction. Upon submission, this should match the
// CTC's expected value otherwise the transaction will revert.
ShouldStartAtElement uint64
// TotalElementsToAppend indicates the number of L2 txs represented by
// this batch. This includes both sequencer and queued txs.
TotalElementsToAppend uint64
// Contexts aggregates redundant L1 block numbers and L1 timestamps for
// the txns encoded in the Tx slice. Further, they specify consecutive
// tx windows in Txs and implicitly allow one to compute how many
// (ommitted) queued txs are in a given window.
Contexts []BatchContext
// Txs contains all sequencer txs that will be recorded in the L1 CTC
// contract.
Txs []*l2types.Transaction
}
// Write encodes the AppendSequencerBatchParams using the following format:
// - should_start_at_element: 5 bytes
// - total_elements_to_append: 3 bytes
// - num_contexts: 3 bytes
// - num_contexts * batch_context: num_contexts * 16 bytes
// - [num txs ommitted]
// - tx_len: 3 bytes
// - tx_bytes: tx_len bytes
func (p *AppendSequencerBatchParams) Write(w *bytes.Buffer) error {
writeUint64(w, p.ShouldStartAtElement, 5)
writeUint64(w, p.TotalElementsToAppend, 3)
// Write number of contexts followed by each fixed-size BatchContext.
writeUint64(w, uint64(len(p.Contexts)), 3)
for _, context := range p.Contexts {
context.Write(w)
}
// Write each length-prefixed tx.
var txBuf bytes.Buffer
for _, tx := range p.Txs {
txBuf.Reset()
if err := tx.EncodeRLP(&txBuf); err != nil {
return err
}
writeUint64(w, uint64(txBuf.Len()), 3)
_, _ = w.Write(txBuf.Bytes()) // can't fail for bytes.Buffer
}
return nil
}
// Serialize performs the same encoding as Write, but returns the resulting
// bytes slice.
func (p *AppendSequencerBatchParams) Serialize() ([]byte, error) {
var buf bytes.Buffer
if err := p.Write(&buf); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// Read decodes the AppendSequencerBatchParams from a bytes stream. If the byte
// stream does not terminate cleanly with an EOF while reading a tx_len, this
// method will return an error. Otherwise, the stream will be parsed according
// to the following format:
// - should_start_at_element: 5 bytes
// - total_elements_to_append: 3 bytes
// - num_contexts: 3 bytes
// - num_contexts * batch_context: num_contexts * 16 bytes
// - [num txs ommitted]
// - tx_len: 3 bytes
// - tx_bytes: tx_len bytes
func (p *AppendSequencerBatchParams) Read(r io.Reader) error {
if err := readUint64(r, &p.ShouldStartAtElement, 5); err != nil {
return err
}
if err := readUint64(r, &p.TotalElementsToAppend, 3); err != nil {
return err
}
// Read number of contexts and deserialize each one.
var numContexts uint64
if err := readUint64(r, &numContexts, 3); err != nil {
return err
}
for i := uint64(0); i < numContexts; i++ {
var batchContext BatchContext
if err := batchContext.Read(r); err != nil {
return err
}
p.Contexts = append(p.Contexts, batchContext)
}
// Deserialize any transactions. Since the number of txs is ommitted
// from the encoding, loop until the stream is consumed.
for {
var txLen uint64
err := readUint64(r, &txLen, 3)
// Getting an EOF when reading the txLen expected for a cleanly
// encoded object. Silece the error and return success.
if err == io.EOF {
return nil
} else if err != nil {
return err
}
tx := new(l2types.Transaction)
if err := tx.DecodeRLP(l2rlp.NewStream(r, txLen)); err != nil {
return err
}
p.Txs = append(p.Txs, tx)
}
}
// writeUint64 writes a the bottom `n` bytes of `val` to `w`.
func writeUint64(w *bytes.Buffer, val uint64, n uint) {
if n < 1 || n > 8 {
panic(fmt.Sprintf("invalid number of bytes %d must be 1-8", n))
}
const maxUint64 uint64 = math.MaxUint64
maxVal := maxUint64 >> (8 * (8 - n))
if val > maxVal {
panic(fmt.Sprintf("cannot encode %d in %d byte value", val, n))
}
var buf [8]byte
byteOrder.PutUint64(buf[:], val)
_, _ = w.Write(buf[8-n:]) // can't fail for bytes.Buffer
}
// readUint64 reads `n` bytes from `r` and returns them in the lower `n` bytes
// of `val`.
func readUint64(r io.Reader, val *uint64, n uint) error {
var buf [8]byte
if _, err := r.Read(buf[8-n:]); err != nil {
return err
}
*val = byteOrder.Uint64(buf[:])
return nil
}