hermez-node/txselector/txselector.go
txselector/txselector.goRaw
1package txselector
2
3// current: very simple version of TxSelector
4
5import (
6 "bytes"
7 "fmt"
8 "math/big"
9 "sort"
10
11 ethCommon "github.com/ethereum/go-ethereum/common"
12 "github.com/hermeznetwork/hermez-node/common"
13 "github.com/hermeznetwork/hermez-node/db/l2db"
14 "github.com/hermeznetwork/hermez-node/db/statedb"
15 "github.com/hermeznetwork/hermez-node/log"
16 "github.com/hermeznetwork/tracerr"
17 "github.com/iden3/go-iden3-crypto/babyjub"
18 "github.com/iden3/go-merkletree/db/pebble"
19)
20
21const (
22 // PathCoordIdxsDB defines the path of the key-value db where the
23 // CoordIdxs will be stored
24 PathCoordIdxsDB = "/coordidxs"
25)
26
27// txs implements the interface Sort for an array of Tx
28type txs []common.PoolL2Tx
29
30func (t txs) Len() int {
31 return len(t)
32}
33func (t txs) Swap(i, j int) {
34 t[i], t[j] = t[j], t[i]
35}
36func (t txs) Less(i, j int) bool {
37 return t[i].AbsoluteFee > t[j].AbsoluteFee
38}
39
40// CoordAccount contains the data of the Coordinator account, that will be used
41// to create new transactions of CreateAccountDeposit type to add new TokenID
42// accounts for the Coordinator to receive the fees.
43type CoordAccount struct {
44 Addr ethCommon.Address
45 BJJ *babyjub.PublicKey
46 AccountCreationAuth []byte
47}
48
49// SelectionConfig contains the parameters of configuration of the selection of
50// transactions for the next batch
51type SelectionConfig struct {
52 // MaxL1UserTxs is the maximum L1-user-tx for a batch
53 MaxL1UserTxs uint64
54 // MaxL1CoordinatorTxs is the maximum L1-coordinator-tx for a batch
55 MaxL1CoordinatorTxs uint64
56
57 // ProcessTxsConfig contains the config for ProcessTxs
58 ProcessTxsConfig statedb.ProcessTxsConfig
59}
60
61// TxSelector implements all the functionalities to select the txs for the next
62// batch
63type TxSelector struct {
64 l2db *l2db.L2DB
65 localAccountsDB *statedb.LocalStateDB
66
67 coordAccount *CoordAccount
68 coordIdxsDB *pebble.PebbleStorage
69}
70
71// NewTxSelector returns a *TxSelector
72func NewTxSelector(coordAccount *CoordAccount, dbpath string,
73 synchronizerStateDB *statedb.StateDB, l2 *l2db.L2DB) (*TxSelector, error) {
74 localAccountsDB, err := statedb.NewLocalStateDB(dbpath,
75 synchronizerStateDB, statedb.TypeTxSelector, 0) // without merkletree
76 if err != nil {
77 return nil, tracerr.Wrap(err)
78 }
79
80 coordIdxsDB, err := pebble.NewPebbleStorage(dbpath+PathCoordIdxsDB, false)
81 if err != nil {
82 return nil, tracerr.Wrap(err)
83 }
84
85 return &TxSelector{
86 l2db: l2,
87 localAccountsDB: localAccountsDB,
88 coordAccount: coordAccount,
89 coordIdxsDB: coordIdxsDB,
90 }, nil
91}
92
93// LocalAccountsDB returns the LocalStateDB of the TxSelector
94func (txsel *TxSelector) LocalAccountsDB() *statedb.LocalStateDB {
95 return txsel.localAccountsDB
96}
97
98// Reset tells the TxSelector to get it's internal AccountsDB
99// from the required `batchNum`
100func (txsel *TxSelector) Reset(batchNum common.BatchNum) error {
101 err := txsel.localAccountsDB.Reset(batchNum, true)
102 if err != nil {
103 return tracerr.Wrap(err)
104 }
105 return nil
106}
107
108// AddCoordIdxs stores the given TokenID with the correspondent Idx to the
109// CoordIdxsDB
110func (txsel *TxSelector) AddCoordIdxs(idxs map[common.TokenID]common.Idx) error {
111 tx, err := txsel.coordIdxsDB.NewTx()
112 if err != nil {
113 return tracerr.Wrap(err)
114 }
115 for tokenID, idx := range idxs {
116 idxBytes, err := idx.Bytes()
117 if err != nil {
118 return tracerr.Wrap(err)
119 }
120 err = tx.Put(tokenID.Bytes(), idxBytes[:])
121 if err != nil {
122 return tracerr.Wrap(err)
123 }
124 }
125 if err := tx.Commit(); err != nil {
126 return tracerr.Wrap(err)
127 }
128 return nil
129}
130
131// GetCoordIdxs returns a map with the stored TokenID with the correspondent
132// Coordinator Idx
133func (txsel *TxSelector) GetCoordIdxs() (map[common.TokenID]common.Idx, error) {
134 r := make(map[common.TokenID]common.Idx)
135 err := txsel.coordIdxsDB.Iterate(func(tokenIDBytes []byte, idxBytes []byte) (bool, error) {
136 idx, err := common.IdxFromBytes(idxBytes)
137 if err != nil {
138 return false, tracerr.Wrap(err)
139 }
140 tokenID, err := common.TokenIDFromBytes(tokenIDBytes)
141 if err != nil {
142 return false, tracerr.Wrap(err)
143 }
144 r[tokenID] = idx
145 return true, nil
146 })
147
148 return r, tracerr.Wrap(err)
149}
150
151// GetL2TxSelection returns the L1CoordinatorTxs and a selection of the L2Txs
152// for the next batch, from the L2DB pool
153func (txsel *TxSelector) GetL2TxSelection(selectionConfig *SelectionConfig,
154 batchNum common.BatchNum) ([]common.Idx, []common.L1Tx, []common.PoolL2Tx, error) {
155 coordIdxs, _, l1CoordinatorTxs, l2Txs, err := txsel.GetL1L2TxSelection(selectionConfig, batchNum,
156 []common.L1Tx{})
157 return coordIdxs, l1CoordinatorTxs, l2Txs, tracerr.Wrap(err)
158}
159
160// GetL1L2TxSelection returns the selection of L1 + L2 txs
161func (txsel *TxSelector) GetL1L2TxSelection(selectionConfig *SelectionConfig,
162 batchNum common.BatchNum, l1Txs []common.L1Tx) ([]common.Idx, []common.L1Tx, []common.L1Tx,
163 []common.PoolL2Tx, error) {
164 // apply l1-user-tx to localAccountDB
165 // create new leaves
166 // update balances
167 // update nonces
168
169 // get existing CoordIdxs
170 coordIdxsMap, err := txsel.GetCoordIdxs()
171 if err != nil {
172 return nil, nil, nil, nil, tracerr.Wrap(err)
173 }
174 var coordIdxs []common.Idx
175 for tokenID := range coordIdxsMap {
176 coordIdxs = append(coordIdxs, coordIdxsMap[tokenID])
177 }
178
179 // get pending l2-tx from tx-pool
180 l2TxsRaw, err := txsel.l2db.GetPendingTxs() // (batchID)
181 if err != nil {
182 return nil, nil, nil, nil, tracerr.Wrap(err)
183 }
184
185 var validTxs txs
186 var l1CoordinatorTxs []common.L1Tx
187 positionL1 := len(l1Txs)
188
189 for i := 0; i < len(l2TxsRaw); i++ {
190 // If tx.ToIdx>=256, tx.ToIdx should exist to localAccountsDB,
191 // if so, tx is used. If tx.ToIdx==0, for an L2Tx will be the
192 // case of TxToEthAddr or TxToBJJ, check if
193 // tx.ToEthAddr/tx.ToBJJ exist in localAccountsDB, if yes tx is
194 // used; if not, check if tx.ToEthAddr is in
195 // AccountCreationAuthDB, if so, tx is used and L1CoordinatorTx
196 // of CreateAccountAndDeposit is created. If tx.ToIdx==1, is a
197 // Exit type and is used.
198 if l2TxsRaw[i].ToIdx == 0 { // ToEthAddr/ToBJJ case
199 validTxs, l1CoordinatorTxs, positionL1, err =
200 txsel.processTxToEthAddrBJJ(validTxs, l1CoordinatorTxs,
201 positionL1, l2TxsRaw[i])
202 if err != nil {
203 log.Debug(err)
204 }
205 } else if l2TxsRaw[i].ToIdx >= common.IdxUserThreshold {
206 _, err = txsel.localAccountsDB.GetAccount(l2TxsRaw[i].ToIdx)
207 if err != nil {
208 // tx not valid
209 log.Debugw("invalid L2Tx: ToIdx not found in StateDB",
210 "ToIdx", l2TxsRaw[i].ToIdx)
211 continue
212 }
213
214 // TODO if EthAddr!=0 or BJJ!=0, check that ToIdxAccount.EthAddr or BJJ
215
216 // Account found in the DB, include the l2Tx in the selection
217 validTxs = append(validTxs, l2TxsRaw[i])
218 } else if l2TxsRaw[i].ToIdx == common.Idx(1) {
219 // valid txs (of Exit type)
220 validTxs = append(validTxs, l2TxsRaw[i])
221 }
222 }
223
224 // get most profitable L2-tx
225 maxL2Txs := selectionConfig.ProcessTxsConfig.MaxTx - uint32(len(l1CoordinatorTxs)) // - len(l1UserTxs) // TODO if there are L1UserTxs take them in to account
226 l2Txs := txsel.getL2Profitable(validTxs, maxL2Txs)
227
228 //nolint:gomnd
229 ptc := statedb.ProcessTxsConfig{ // TODO TMP
230 NLevels: 32,
231 MaxFeeTx: 64,
232 MaxTx: 512,
233 MaxL1Tx: 64,
234 }
235 // process the txs in the local AccountsDB
236 _, err = txsel.localAccountsDB.ProcessTxs(ptc, coordIdxs, l1Txs, l1CoordinatorTxs, l2Txs)
237 if err != nil {
238 return nil, nil, nil, nil, tracerr.Wrap(err)
239 }
240 err = txsel.localAccountsDB.MakeCheckpoint()
241 if err != nil {
242 return nil, nil, nil, nil, tracerr.Wrap(err)
243 }
244
245 return nil, l1Txs, l1CoordinatorTxs, l2Txs, nil
246}
247
248// processTxsToEthAddrBJJ process the common.PoolL2Tx in the case where
249// ToIdx==0, which can be the tx type of ToEthAddr or ToBJJ. If the receiver
250// does not have an account yet, a new L1CoordinatorTx of type
251// CreateAccountDeposit (with 0 as DepositAmount) is created and added to the
252// l1CoordinatorTxs array, and then the PoolL2Tx is added into the validTxs
253// array.
254func (txsel *TxSelector) processTxToEthAddrBJJ(validTxs txs, l1CoordinatorTxs []common.L1Tx,
255 positionL1 int, l2Tx common.PoolL2Tx) (txs, []common.L1Tx, int, error) {
256 // if L2Tx needs a new L1CoordinatorTx of CreateAccount type, and a
257 // previous L2Tx in the current process already created a
258 // L1CoordinatorTx of this type, in the DB there still seem that needs
259 // to create a new L1CoordinatorTx, but as is already created, the tx
260 // is valid
261 if checkAlreadyPendingToCreate(l1CoordinatorTxs, l2Tx.ToEthAddr, l2Tx.ToBJJ) {
262 validTxs = append(validTxs, l2Tx)
263 return validTxs, l1CoordinatorTxs, positionL1, nil
264 }
265
266 if !bytes.Equal(l2Tx.ToEthAddr.Bytes(), common.EmptyAddr.Bytes()) &&
267 !bytes.Equal(l2Tx.ToEthAddr.Bytes(), common.FFAddr.Bytes()) {
268 // case: ToEthAddr != 0x00 neither 0xff
269 var accAuth *common.AccountCreationAuth
270 if l2Tx.ToBJJ != nil {
271 // case: ToBJJ!=0:
272 // if idx exist for EthAddr&BJJ use it
273 _, err := txsel.localAccountsDB.GetIdxByEthAddrBJJ(l2Tx.ToEthAddr,
274 l2Tx.ToBJJ, l2Tx.TokenID)
275 if err == nil {
276 // account for ToEthAddr&ToBJJ already exist,
277 // there is no need to create a new one.
278 // tx valid, StateDB will use the ToIdx==0 to define the AuxToIdx
279 validTxs = append(validTxs, l2Tx)
280 return validTxs, l1CoordinatorTxs, positionL1, nil
281 }
282 // if not, check if AccountCreationAuth exist for that
283 // ToEthAddr
284 accAuth, err = txsel.l2db.GetAccountCreationAuth(l2Tx.ToEthAddr)
285 if err != nil {
286 // not found, l2Tx will not be added in the selection
287 return validTxs, l1CoordinatorTxs, positionL1, tracerr.Wrap(fmt.Errorf("invalid L2Tx: ToIdx not found in StateDB, neither ToEthAddr found in AccountCreationAuths L2DB. ToIdx: %d, ToEthAddr: %s",
288 l2Tx.ToIdx, l2Tx.ToEthAddr.Hex()))
289 }
290 if accAuth.BJJ != l2Tx.ToBJJ {
291 // if AccountCreationAuth.BJJ is not the same
292 // than in the tx, tx is not accepted
293 return validTxs, l1CoordinatorTxs, positionL1, tracerr.Wrap(fmt.Errorf("invalid L2Tx: ToIdx not found in StateDB, neither ToEthAddr & ToBJJ found in AccountCreationAuths L2DB. ToIdx: %d, ToEthAddr: %s, ToBJJ: %s",
294 l2Tx.ToIdx, l2Tx.ToEthAddr.Hex(), l2Tx.ToBJJ.String()))
295 }
296 validTxs = append(validTxs, l2Tx)
297 } else {
298 // case: ToBJJ==0:
299 // if idx exist for EthAddr use it
300 _, err := txsel.localAccountsDB.GetIdxByEthAddr(l2Tx.ToEthAddr, l2Tx.TokenID)
301 if err == nil {
302 // account for ToEthAddr already exist,
303 // there is no need to create a new one.
304 // tx valid, StateDB will use the ToIdx==0 to define the AuxToIdx
305 validTxs = append(validTxs, l2Tx)
306 return validTxs, l1CoordinatorTxs, positionL1, nil
307 }
308 // if not, check if AccountCreationAuth exist for that ToEthAddr
309 accAuth, err = txsel.l2db.GetAccountCreationAuth(l2Tx.ToEthAddr)
310 if err != nil {
311 // not found, l2Tx will not be added in the selection
312 return validTxs, l1CoordinatorTxs, positionL1, tracerr.Wrap(fmt.Errorf("invalid L2Tx: ToIdx not found in StateDB, neither ToEthAddr found in AccountCreationAuths L2DB. ToIdx: %d, ToEthAddr: %s",
313 l2Tx.ToIdx, l2Tx.ToEthAddr))
314 }
315 validTxs = append(validTxs, l2Tx)
316 }
317 // create L1CoordinatorTx for the accountCreation
318 l1CoordinatorTx := common.L1Tx{
319 Position: positionL1,
320 UserOrigin: false,
321 FromEthAddr: accAuth.EthAddr,
322 FromBJJ: accAuth.BJJ,
323 TokenID: l2Tx.TokenID,
324 DepositAmount: big.NewInt(0),
325 Type: common.TxTypeCreateAccountDeposit,
326 }
327 positionL1++
328 l1CoordinatorTxs = append(l1CoordinatorTxs, l1CoordinatorTx)
329 } else if bytes.Equal(l2Tx.ToEthAddr.Bytes(), common.FFAddr.Bytes()) && l2Tx.ToBJJ != nil {
330 // if idx exist for EthAddr&BJJ use it
331 _, err := txsel.localAccountsDB.GetIdxByEthAddrBJJ(l2Tx.ToEthAddr, l2Tx.ToBJJ,
332 l2Tx.TokenID)
333 if err == nil {
334 // account for ToEthAddr&ToBJJ already exist, (where ToEthAddr==0xff)
335 // there is no need to create a new one.
336 // tx valid, StateDB will use the ToIdx==0 to define the AuxToIdx
337 validTxs = append(validTxs, l2Tx)
338 return validTxs, l1CoordinatorTxs, positionL1, nil
339 }
340 // if idx don't exist for EthAddr&BJJ,
341 // coordinator can create a new account without
342 // L1Authorization, as ToEthAddr==0xff
343 // create L1CoordinatorTx for the accountCreation
344 l1CoordinatorTx := common.L1Tx{
345 Position: positionL1,
346 UserOrigin: false,
347 FromEthAddr: l2Tx.ToEthAddr,
348 FromBJJ: l2Tx.ToBJJ,
349 TokenID: l2Tx.TokenID,
350 DepositAmount: big.NewInt(0),
351 Type: common.TxTypeCreateAccountDeposit,
352 }
353 positionL1++
354 l1CoordinatorTxs = append(l1CoordinatorTxs, l1CoordinatorTx)
355 }
356
357 return validTxs, l1CoordinatorTxs, positionL1, nil
358}
359
360func checkAlreadyPendingToCreate(l1CoordinatorTxs []common.L1Tx,
361 addr ethCommon.Address, bjj *babyjub.PublicKey) bool {
362 for i := 0; i < len(l1CoordinatorTxs); i++ {
363 if bytes.Equal(l1CoordinatorTxs[i].FromEthAddr.Bytes(), addr.Bytes()) {
364 if bjj == nil {
365 return true
366 }
367 if l1CoordinatorTxs[i].FromBJJ == bjj {
368 return true
369 }
370 }
371 }
372 return false
373}
374
375// getL2Profitable returns the profitable selection of L2Txssorted by Nonce
376func (txsel *TxSelector) getL2Profitable(txs txs, max uint32) txs {
377 sort.Sort(txs)
378 if len(txs) < int(max) {
379 return txs
380 }
381 txs = txs[:max]
382
383 // sort l2Txs by Nonce. This can be done in many different ways, what
384 // is needed is to output the txs where the Nonce of txs for each
385 // Account is sorted, but the txs can not be grouped by sender Account
386 // neither by Fee. This is because later on the Nonces will need to be
387 // sequential for the zkproof generation.
388 sort.SliceStable(txs, func(i, j int) bool {
389 return txs[i].Nonce < txs[j].Nonce
390 })
391
392 return txs
393}