hermez-node/coordinator/coordinator.go
coordinator/coordinator.goRaw
1package coordinator
2
3import (
4 "context"
5 "fmt"
6 "time"
7
8 ethCommon "github.com/ethereum/go-ethereum/common"
9 "github.com/ethereum/go-ethereum/core/types"
10 "github.com/hermeznetwork/hermez-node/batchbuilder"
11 "github.com/hermeznetwork/hermez-node/common"
12 "github.com/hermeznetwork/hermez-node/db/historydb"
13 "github.com/hermeznetwork/hermez-node/eth"
14 "github.com/hermeznetwork/hermez-node/log"
15 "github.com/hermeznetwork/hermez-node/txselector"
16)
17
18var errTODO = fmt.Errorf("TODO")
19
20// ErrStop is returned when the function is stopped asynchronously via the stop
21// channel. It doesn't indicate an error.
22var ErrStop = fmt.Errorf("Stopped")
23
24// Config contains the Coordinator configuration
25type Config struct {
26 ForgerAddress ethCommon.Address
27}
28
29// Coordinator implements the Coordinator type
30type Coordinator struct {
31 forging bool
32 // rw *sync.RWMutex
33 // isForgeSeq bool // WIP just for testing while implementing
34
35 config Config
36
37 batchNum common.BatchNum
38 serverProofPool *ServerProofPool
39
40 // synchronizer *synchronizer.Synchronizer
41 hdb *historydb.HistoryDB
42 txsel *txselector.TxSelector
43 batchBuilder *batchbuilder.BatchBuilder
44
45 ethClient eth.ClientInterface
46 ethTxs []*types.Transaction
47 // ethTxStore kvdb.Storage
48}
49
50// NewCoordinator creates a new Coordinator
51func NewCoordinator(conf Config,
52 hdb *historydb.HistoryDB,
53 txsel *txselector.TxSelector,
54 bb *batchbuilder.BatchBuilder,
55 serverProofs []ServerProofInterface,
56 ethClient eth.ClientInterface) *Coordinator { // once synchronizer is ready, synchronizer.Synchronizer will be passed as parameter here
57 serverProofPool := NewServerProofPool(len(serverProofs))
58 for _, serverProof := range serverProofs {
59 serverProofPool.Add(serverProof)
60 }
61 c := Coordinator{
62 config: conf,
63 serverProofPool: serverProofPool,
64 hdb: hdb,
65 txsel: txsel,
66 batchBuilder: bb,
67 ethClient: ethClient,
68
69 ethTxs: make([]*types.Transaction, 0),
70 // ethTxStore: memory.NewMemoryStorage(),
71 // rw: &sync.RWMutex{},
72 }
73 return &c
74}
75
76// TODO(Edu): Change the current design of the coordinator structur:
77// - Move Start and Stop functions (from node/node.go) here
78// - Add concept of StartPipeline, StopPipeline, that spawns and stops the goroutines
79// - Add a Manager that calls StartPipeline and StopPipeline, checks when it's time to forge, schedules new batches, etc.
80// - Add a TxMonitor that monitors successful ForgeBatch ethereum transactions and waits for N blocks of confirmation, and reports back errors to the Manager.
81
82// ForgeLoopFn is the function ran in a loop that checks if it's time to forge
83// and forges a batch if so and sends it to outBatchCh. Returns true if it's
84// the coordinator turn to forge.
85func (c *Coordinator) ForgeLoopFn(outBatchCh chan *BatchInfo, stopCh chan bool) (forgetime bool, err error) {
86 // TODO: Move the logic to check if it's forge time or not outside the pipeline
87 isForgeSequence, err := c.isForgeSequence()
88 if err != nil {
89 return false, err
90 }
91 if !isForgeSequence {
92 if c.forging {
93 log.Info("ForgeLoopFn: forging state end")
94 c.forging = false
95 }
96 log.Debug("ForgeLoopFn: not in forge time")
97 return false, nil
98 }
99 log.Debug("ForgeLoopFn: forge time")
100 if !c.forging {
101 // Start pipeline from a batchNum state taken from synchronizer
102 log.Info("ForgeLoopFn: forging state begin")
103 // c.batchNum = c.hdb.GetLastBatchNum() // uncomment when HistoryDB is ready
104 err := c.txsel.Reset(c.batchNum)
105 if err != nil {
106 log.Errorw("ForgeLoopFn: TxSelector.Reset", "error", err)
107 return true, err
108 }
109 err = c.batchBuilder.Reset(c.batchNum, true)
110 if err != nil {
111 log.Errorw("ForgeLoopFn: BatchBuilder.Reset", "error", err)
112 return true, err
113 }
114 // c.batchQueue = NewBatchQueue()
115 c.forging = true
116 }
117 // TODO once synchronizer has this method ready:
118 // If there's been a reorg, handle it
119 // handleReorg() function decides if the reorg must restart the pipeline or not
120 // if c.synchronizer.Reorg():
121 _ = c.handleReorg()
122
123 defer func() {
124 if err == ErrStop {
125 log.Info("ForgeLoopFn: forgeLoopFn stopped")
126 }
127 }()
128
129 // 0. Wait for an available server proof
130 // blocking call
131 serverProof, err := c.serverProofPool.Get(stopCh)
132 if err != nil {
133 return true, err
134 }
135 defer func() {
136 if !forgetime || err != nil {
137 c.serverProofPool.Add(serverProof)
138 }
139 }()
140
141 log.Debugw("ForgeLoopFn: using serverProof", "server", serverProof)
142 log.Debugw("ForgeLoopFn: forge start")
143 // forge for batchNum = batchNum + 1.
144 batchInfo, err := c.forge(serverProof)
145 if err != nil {
146 log.Errorw("forge", "error", err)
147 return true, err
148 }
149 log.Debugw("ForgeLoopFn: forge end", "batchNum", batchInfo.batchNum)
150 outBatchCh <- batchInfo
151 return true, nil
152}
153
154// GetProofCallForgeLoopFn is the function ran in a loop that gets a forged
155// batch via inBatchCh, waits for the proof server to finish, calls the ForgeBatch
156// function in the Rollup Smart Contract, and sends the batch to outBatchCh.
157func (c *Coordinator) GetProofCallForgeLoopFn(inBatchCh, outBatchCh chan *BatchInfo, stopCh chan bool) (err error) {
158 defer func() {
159 if err == ErrStop {
160 log.Info("GetProofCallForgeLoopFn: forgeLoopFn stopped")
161 }
162 }()
163 select {
164 case <-stopCh:
165 return ErrStop
166 case batchInfo := <-inBatchCh:
167 log.Debugw("GetProofCallForgeLoopFn: getProofCallForge start", "batchNum", batchInfo.batchNum)
168 if err := c.getProofCallForge(batchInfo, stopCh); err != nil {
169 return err
170 }
171 log.Debugw("GetProofCallForgeLoopFn: getProofCallForge end", "batchNum", batchInfo.batchNum)
172 outBatchCh <- batchInfo
173 }
174 return nil
175}
176
177// ForgeCallConfirmLoopFn is the function ran in a loop that gets a batch that
178// has been sent to the Rollup Smart Contract via inBatchCh and waits for the
179// ethereum transaction confirmation.
180func (c *Coordinator) ForgeCallConfirmLoopFn(inBatchCh chan *BatchInfo, stopCh chan bool) (err error) {
181 defer func() {
182 if err == ErrStop {
183 log.Info("ForgeCallConfirmLoopFn: forgeConfirmLoopFn stopped")
184 }
185 }()
186 select {
187 case <-stopCh:
188 return ErrStop
189 case batchInfo := <-inBatchCh:
190 log.Debugw("ForgeCallConfirmLoopFn: forgeCallConfirm start", "batchNum", batchInfo.batchNum)
191 if err := c.forgeCallConfirm(batchInfo, stopCh); err != nil {
192 return err
193 }
194 log.Debugw("ForgeCallConfirmLoopFn: forgeCallConfirm end", "batchNum", batchInfo.batchNum)
195 }
196 return nil
197}
198
199func (c *Coordinator) forge(serverProof ServerProofInterface) (*BatchInfo, error) {
200 // remove transactions from the pool that have been there for too long
201 err := c.purgeRemoveByTimeout()
202 if err != nil {
203 return nil, err
204 }
205
206 c.batchNum = c.batchNum + 1
207 batchInfo := NewBatchInfo(c.batchNum, serverProof) // to accumulate metadata of the batch
208
209 var poolL2Txs []common.PoolL2Tx
210 // var feesInfo
211 var l1UserTxsExtra, l1OperatorTxs []common.L1Tx
212 // 1. Decide if we forge L2Tx or L1+L2Tx
213 if c.shouldL1L2Batch() {
214 // 2a: L1+L2 txs
215 // l1UserTxs, toForgeL1TxsNumber := c.hdb.GetNextL1UserTxs() // TODO once HistoryDB is ready, uncomment
216 var l1UserTxs []common.L1Tx = nil // tmp, depends on HistoryDB
217 l1UserTxsExtra, l1OperatorTxs, poolL2Txs, err = c.txsel.GetL1L2TxSelection(c.batchNum, l1UserTxs) // TODO once feesInfo is added to method return, add the var
218 if err != nil {
219 return nil, err
220 }
221 } else {
222 // 2b: only L2 txs
223 poolL2Txs, err = c.txsel.GetL2TxSelection(c.batchNum) // TODO once feesInfo is added to method return, add the var
224 if err != nil {
225 return nil, err
226 }
227 l1UserTxsExtra = nil
228 l1OperatorTxs = nil
229 }
230
231 // Run purger to invalidate transactions that become invalid beause of
232 // the poolL2Txs selected. Will mark as invalid the txs that have a
233 // (fromIdx, nonce) which already appears in the selected txs (includes
234 // all the nonces smaller than the current one)
235 err = c.purgeInvalidDueToL2TxsSelection(poolL2Txs)
236 if err != nil {
237 return nil, err
238 }
239
240 // 3. Save metadata from TxSelector output for BatchNum
241 batchInfo.SetTxsInfo(l1UserTxsExtra, l1OperatorTxs, poolL2Txs) // TODO feesInfo
242
243 // 4. Call BatchBuilder with TxSelector output
244 configBatch := &batchbuilder.ConfigBatch{
245 ForgerAddress: c.config.ForgerAddress,
246 }
247 zkInputs, err := c.batchBuilder.BuildBatch(configBatch, l1UserTxsExtra, l1OperatorTxs, poolL2Txs, nil) // TODO []common.TokenID --> feesInfo
248 if err != nil {
249 return nil, err
250 }
251
252 // 5. Save metadata from BatchBuilder output for BatchNum
253 batchInfo.SetZKInputs(zkInputs)
254
255 // 6. Call an idle server proof with BatchBuilder output, save server proof info for batchNum
256 err = batchInfo.serverProof.CalculateProof(zkInputs)
257 if err != nil {
258 return nil, err
259 }
260
261 return &batchInfo, nil
262}
263
264// getProofCallForge gets the generated zkProof & sends it to the SmartContract
265func (c *Coordinator) getProofCallForge(batchInfo *BatchInfo, stopCh chan bool) error {
266 serverProof := batchInfo.serverProof
267 proof, err := serverProof.GetProof(stopCh) // blocking call, until not resolved don't continue. Returns when the proof server has calculated the proof
268 c.serverProofPool.Add(serverProof)
269 batchInfo.serverProof = nil
270 if err != nil {
271 return err
272 }
273 batchInfo.SetProof(proof)
274 forgeBatchArgs := c.prepareForgeBatchArgs(batchInfo)
275 ethTx, err := c.ethClient.RollupForgeBatch(forgeBatchArgs)
276 if err != nil {
277 return err
278 }
279 // TODO: Move this to the next step (forgeCallConfirm)
280 log.Debugf("ethClient ForgeCall sent, batchNum: %d", c.batchNum)
281 batchInfo.SetEthTx(ethTx)
282
283 // TODO(FUTURE) once tx data type is defined, store ethTx (returned by ForgeCall)
284 // TBD if use ethTxStore as a disk k-v database, or use a Queue
285 // tx, err := c.ethTxStore.NewTx()
286 // if err != nil {
287 // return err
288 // }
289 // tx.Put(ethTx.Hash(), ethTx.Bytes())
290 // if err := tx.Commit(); err!=nil {
291 // return nil
292 // }
293
294 return nil
295}
296
297func (c *Coordinator) forgeCallConfirm(batchInfo *BatchInfo, stopCh chan bool) error {
298 // TODO strategy of this sequence TBD
299 // confirm eth txs and mark them as accepted sequence
300 // IDEA: Keep an array in Coordinator with the list of sent ethTx.
301 // Here, loop over them and only delete them once the number of
302 // confirmed blocks is over a configured value. If the tx is rejected,
303 // return error.
304 // ethTx := ethTxStore.GetFirstPending()
305 // waitForAccepted(ethTx) // blocking call, returns once the ethTx is mined
306 // ethTxStore.MarkAccepted(ethTx)
307 txID := batchInfo.ethTx.Hash()
308 // TODO: Follow EthereumClient.waitReceipt logic
309 count := 0
310 // TODO: Define this waitTime in the config
311 waitTime := 100 * time.Millisecond //nolint:gomnd
312 select {
313 case <-time.After(waitTime):
314 receipt, err := c.ethClient.EthTransactionReceipt(context.TODO(), txID)
315 if err != nil {
316 return err
317 }
318 if receipt != nil {
319 if receipt.Status == types.ReceiptStatusFailed {
320 return fmt.Errorf("receipt status is failed")
321 } else if receipt.Status == types.ReceiptStatusSuccessful {
322 return nil
323 }
324 }
325 // TODO: Call go-ethereum:
326 // if err == nil && receipt == nil :
327 // `func (ec *Client) TransactionByHash(ctx context.Context, hash common.Hash) (tx *types.Transaction, isPending bool, err error) {`
328 count++
329 if time.Duration(count)*waitTime > 60*time.Second {
330 log.Warnw("Waiting for ethTx receipt for more than 60 seconds", "tx", batchInfo.ethTx)
331 // TODO: Decide if we resend the Tx with higher gas price
332 }
333 case <-stopCh:
334 return ErrStop
335 }
336 return fmt.Errorf("timeout")
337}
338
339func (c *Coordinator) handleReorg() error {
340 return nil // TODO
341}
342
343// isForgeSequence returns true if the node is the Forger in the current ethereum block
344func (c *Coordinator) isForgeSequence() (bool, error) {
345 // TODO: Consider checking if we can forge by quering the Synchronizer instead of using ethClient
346 blockNum, err := c.ethClient.EthCurrentBlock()
347 if err != nil {
348 return false, err
349 }
350 addr, err := c.ethClient.EthAddress()
351 if err != nil {
352 return false, err
353 }
354 return c.ethClient.AuctionCanForge(*addr, blockNum+1)
355}
356
357func (c *Coordinator) purgeRemoveByTimeout() error {
358 return nil // TODO
359}
360
361func (c *Coordinator) purgeInvalidDueToL2TxsSelection(l2Txs []common.PoolL2Tx) error {
362 return nil // TODO
363}
364
365func (c *Coordinator) shouldL1L2Batch() bool {
366 return false // TODO
367}
368
369func (c *Coordinator) prepareForgeBatchArgs(batchInfo *BatchInfo) *eth.RollupForgeBatchArgs {
370 // TODO
371 return ð.RollupForgeBatchArgs{}
372}