hermez-node/test/transakcio/txs.go
test/transakcio/txs.goRaw
1package transakcio
2
3import (
4 "crypto/ecdsa"
5 "fmt"
6 "math/big"
7 "strconv"
8 "strings"
9 "time"
10
11 ethCommon "github.com/ethereum/go-ethereum/common"
12 ethCrypto "github.com/ethereum/go-ethereum/crypto"
13 "github.com/hermeznetwork/hermez-node/common"
14 "github.com/hermeznetwork/hermez-node/log"
15 "github.com/iden3/go-iden3-crypto/babyjub"
16)
17
18// TestContext contains the data of the test
19type TestContext struct {
20 Instructions []instruction
21 accountsNames []string
22 Users map[string]*User
23 lastRegisteredTokenID common.TokenID
24 l1CreatedAccounts map[string]*Account
25
26 // rollupConstMaxL1UserTx Maximum L1-user transactions allowed to be queued in a batch
27 rollupConstMaxL1UserTx int
28
29 idx int
30 currBlock BlockData
31 currBatch BatchData
32 currBatchNum int
33 queues [][]L1Tx
34 toForgeNum int
35 openToForge int
36}
37
38// NewTestContext returns a new TestContext
39func NewTestContext(rollupConstMaxL1UserTx int) *TestContext {
40 return &TestContext{
41 Users: make(map[string]*User),
42 l1CreatedAccounts: make(map[string]*Account),
43 lastRegisteredTokenID: 0,
44
45 rollupConstMaxL1UserTx: rollupConstMaxL1UserTx,
46 idx: common.UserThreshold,
47 currBatchNum: 0,
48 // start with 2 queues, one for toForge, and the other for openToForge
49 queues: make([][]L1Tx, 2),
50 toForgeNum: 0,
51 openToForge: 1,
52 }
53}
54
55// Account contains the data related to the account for a specific TokenID of a User
56type Account struct {
57 Idx common.Idx
58 Nonce common.Nonce
59}
60
61// User contains the data related to a testing user
62type User struct {
63 BJJ *babyjub.PrivateKey
64 Addr ethCommon.Address
65 Accounts map[common.TokenID]*Account
66}
67
68// BlockData contains the information of a Block
69type BlockData struct {
70 // block *common.Block // ethereum block
71 // L1UserTxs that were accepted in the block
72 L1UserTxs []common.L1Tx
73 Batches []BatchData
74 RegisteredTokens []common.Token
75}
76
77// BatchData contains the information of a Batch
78type BatchData struct {
79 L1Batch bool // TODO: Remove once Batch.ForgeL1TxsNum is a pointer
80 L1CoordinatorTxs []common.L1Tx
81 testL1CoordinatorTxs []L1Tx
82 L2Txs []common.L2Tx
83 // testL2Tx are L2Txs without the Idx&EthAddr&BJJ setted, but with the
84 // string that represents the account
85 testL2Txs []L2Tx
86 CreatedAccounts []common.Account
87}
88
89// L1Tx is the data structure used internally for transaction test generation,
90// which contains a common.L1Tx data plus some intermediate data for the
91// transaction generation.
92type L1Tx struct {
93 lineNum int
94 fromIdxName string
95 toIdxName string
96
97 L1Tx common.L1Tx
98}
99
100// L2Tx is the data structure used internally for transaction test generation,
101// which contains a common.L2Tx data plus some intermediate data for the
102// transaction generation.
103type L2Tx struct {
104 lineNum int
105 fromIdxName string
106 toIdxName string
107 tokenID common.TokenID
108 L2Tx common.L2Tx
109}
110
111// GenerateBlocks returns an array of BlockData for a given set. It uses the
112// accounts (keys & nonces) of the TestContext.
113func (tc *TestContext) GenerateBlocks(set string) ([]BlockData, error) {
114 parser := newParser(strings.NewReader(set))
115 parsedSet, err := parser.parse()
116 if err != nil {
117 return nil, err
118 }
119
120 tc.Instructions = parsedSet.instructions
121 tc.accountsNames = parsedSet.accounts
122
123 tc.generateKeys(tc.accountsNames)
124
125 var blocks []BlockData
126 for _, inst := range parsedSet.instructions {
127 switch inst.typ {
128 case txTypeCreateAccountDepositCoordinator:
129 if err := tc.checkIfTokenIsRegistered(inst); err != nil {
130 log.Error(err)
131 return nil, fmt.Errorf("Line %d: %s", inst.lineNum, err.Error())
132 }
133 tx := common.L1Tx{
134 FromEthAddr: tc.Users[inst.from].Addr,
135 FromBJJ: tc.Users[inst.from].BJJ.Public(),
136 TokenID: inst.tokenID,
137 LoadAmount: big.NewInt(int64(inst.loadAmount)),
138 Type: common.TxTypeCreateAccountDeposit, // as txTypeCreateAccountDepositCoordinator is not valid oustide Transakcio package
139 }
140 testTx := L1Tx{
141 lineNum: inst.lineNum,
142 fromIdxName: inst.from,
143 L1Tx: tx,
144 }
145 tc.currBatch.testL1CoordinatorTxs = append(tc.currBatch.testL1CoordinatorTxs, testTx)
146 case common.TxTypeCreateAccountDeposit, common.TxTypeCreateAccountDepositTransfer:
147 if err := tc.checkIfTokenIsRegistered(inst); err != nil {
148 log.Error(err)
149 return nil, fmt.Errorf("Line %d: %s", inst.lineNum, err.Error())
150 }
151 tx := common.L1Tx{
152 FromEthAddr: tc.Users[inst.from].Addr,
153 FromBJJ: tc.Users[inst.from].BJJ.Public(),
154 TokenID: inst.tokenID,
155 LoadAmount: big.NewInt(int64(inst.loadAmount)),
156 Type: inst.typ,
157 }
158 if inst.typ == common.TxTypeCreateAccountDepositTransfer {
159 tx.Amount = big.NewInt(int64(inst.amount))
160 }
161 testTx := L1Tx{
162 lineNum: inst.lineNum,
163 fromIdxName: inst.from,
164 toIdxName: inst.to,
165 L1Tx: tx,
166 }
167 tc.addToL1Queue(testTx)
168 case common.TxTypeDeposit, common.TxTypeDepositTransfer:
169 if err := tc.checkIfTokenIsRegistered(inst); err != nil {
170 log.Error(err)
171 return nil, fmt.Errorf("Line %d: %s", inst.lineNum, err.Error())
172 }
173 if err := tc.checkIfAccountExists(inst.from, inst); err != nil {
174 log.Error(err)
175 return nil, fmt.Errorf("Line %d: %s", inst.lineNum, err.Error())
176 }
177 tx := common.L1Tx{
178 TokenID: inst.tokenID,
179 LoadAmount: big.NewInt(int64(inst.loadAmount)),
180 Type: inst.typ,
181 }
182 if inst.typ == common.TxTypeDepositTransfer {
183 tx.Amount = big.NewInt(int64(inst.amount))
184 }
185 testTx := L1Tx{
186 lineNum: inst.lineNum,
187 fromIdxName: inst.from,
188 toIdxName: inst.to,
189 L1Tx: tx,
190 }
191 tc.addToL1Queue(testTx)
192 case common.TxTypeTransfer:
193 if err := tc.checkIfTokenIsRegistered(inst); err != nil {
194 log.Error(err)
195 return nil, fmt.Errorf("Line %d: %s", inst.lineNum, err.Error())
196 }
197 tx := common.L2Tx{
198 Amount: big.NewInt(int64(inst.amount)),
199 Fee: common.FeeSelector(inst.fee),
200 Type: common.TxTypeTransfer,
201 }
202 nTx, err := common.NewPoolL2Tx(tx.PoolL2Tx())
203 if err != nil {
204 return nil, fmt.Errorf("Line %d: %s", inst.lineNum, err.Error())
205 }
206 tx = nTx.L2Tx()
207 tx.BatchNum = common.BatchNum(tc.currBatchNum) // when converted to PoolL2Tx BatchNum parameter is lost
208 testTx := L2Tx{
209 lineNum: inst.lineNum,
210 fromIdxName: inst.from,
211 toIdxName: inst.to,
212 tokenID: inst.tokenID,
213 L2Tx: tx,
214 }
215 tc.currBatch.testL2Txs = append(tc.currBatch.testL2Txs, testTx)
216 case common.TxTypeExit:
217 if err := tc.checkIfTokenIsRegistered(inst); err != nil {
218 log.Error(err)
219 return nil, fmt.Errorf("Line %d: %s", inst.lineNum, err.Error())
220 }
221 tx := common.L2Tx{
222 ToIdx: common.Idx(1), // as is an Exit
223 Amount: big.NewInt(int64(inst.amount)),
224 Type: common.TxTypeExit,
225 }
226 tx.BatchNum = common.BatchNum(tc.currBatchNum) // when converted to PoolL2Tx BatchNum parameter is lost
227 testTx := L2Tx{
228 lineNum: inst.lineNum,
229 fromIdxName: inst.from,
230 toIdxName: inst.to,
231 tokenID: inst.tokenID,
232 L2Tx: tx,
233 }
234 tc.currBatch.testL2Txs = append(tc.currBatch.testL2Txs, testTx)
235 case common.TxTypeForceExit:
236 if err := tc.checkIfTokenIsRegistered(inst); err != nil {
237 log.Error(err)
238 return nil, fmt.Errorf("Line %d: %s", inst.lineNum, err.Error())
239 }
240 tx := common.L1Tx{
241 ToIdx: common.Idx(1), // as is an Exit
242 TokenID: inst.tokenID,
243 Amount: big.NewInt(int64(inst.amount)),
244 Type: common.TxTypeExit,
245 }
246 testTx := L1Tx{
247 lineNum: inst.lineNum,
248 fromIdxName: inst.from,
249 toIdxName: inst.to,
250 L1Tx: tx,
251 }
252 tc.addToL1Queue(testTx)
253 case typeNewBatch:
254 if err = tc.calculateIdxForL1Txs(true, tc.currBatch.testL1CoordinatorTxs); err != nil {
255 return nil, err
256 }
257 if err = tc.setIdxs(); err != nil {
258 log.Error(err)
259 return nil, err
260 }
261 case typeNewBatchL1:
262 // for each L1UserTx of the queues[ToForgeNum], calculate the Idx
263 if err = tc.calculateIdxForL1Txs(false, tc.queues[tc.toForgeNum]); err != nil {
264 return nil, err
265 }
266 if err = tc.calculateIdxForL1Txs(true, tc.currBatch.testL1CoordinatorTxs); err != nil {
267 return nil, err
268 }
269
270 // once Idxs are calculated, update transactions to use the new Idxs
271 for i := 0; i < len(tc.queues[tc.toForgeNum]); i++ {
272 testTx := &tc.queues[tc.toForgeNum][i]
273 if testTx.L1Tx.Type != common.TxTypeCreateAccountDeposit && testTx.L1Tx.Type != common.TxTypeCreateAccountDepositTransfer {
274 testTx.L1Tx.FromIdx = tc.Users[testTx.fromIdxName].Accounts[testTx.L1Tx.TokenID].Idx
275 }
276 testTx.L1Tx.FromEthAddr = tc.Users[testTx.fromIdxName].Addr
277 testTx.L1Tx.FromBJJ = tc.Users[testTx.fromIdxName].BJJ.Public()
278 if testTx.toIdxName == "" {
279 testTx.L1Tx.ToIdx = common.Idx(0)
280 } else {
281 testTx.L1Tx.ToIdx = tc.Users[testTx.toIdxName].Accounts[testTx.L1Tx.TokenID].Idx
282 }
283 if testTx.L1Tx.Type == common.TxTypeExit {
284 testTx.L1Tx.ToIdx = common.Idx(1)
285 }
286 bn := common.BatchNum(tc.currBatchNum)
287 testTx.L1Tx.BatchNum = &bn
288 nTx, err := common.NewL1Tx(&testTx.L1Tx)
289 if err != nil {
290 return nil, fmt.Errorf("Line %d: %s", testTx.lineNum, err.Error())
291 }
292 testTx.L1Tx = *nTx
293
294 tc.currBlock.L1UserTxs = append(tc.currBlock.L1UserTxs, testTx.L1Tx)
295 }
296 if err = tc.setIdxs(); err != nil {
297 log.Error(err)
298 return nil, err
299 }
300 // advance batch
301 tc.toForgeNum++
302 if tc.toForgeNum == tc.openToForge {
303 tc.openToForge++
304 newQueue := []L1Tx{}
305 tc.queues = append(tc.queues, newQueue)
306 }
307 case typeNewBlock:
308 blocks = append(blocks, tc.currBlock)
309 tc.currBlock = BlockData{}
310 case typeRegisterToken:
311 newToken := common.Token{
312 TokenID: inst.tokenID,
313 EthBlockNum: int64(len(blocks)),
314 }
315 if inst.tokenID != tc.lastRegisteredTokenID+1 {
316 return nil, fmt.Errorf("Line %d: RegisterToken TokenID should be sequential, expected TokenID: %d, defined TokenID: %d", inst.lineNum, tc.lastRegisteredTokenID+1, inst.tokenID)
317 }
318 tc.lastRegisteredTokenID++
319 tc.currBlock.RegisteredTokens = append(tc.currBlock.RegisteredTokens, newToken)
320 default:
321 return nil, fmt.Errorf("Line %d: Unexpected type: %s", inst.lineNum, inst.typ)
322 }
323 }
324
325 return blocks, nil
326}
327
328// calculateIdxsForL1Txs calculates new Idx for new created accounts. If
329// 'isCoordinatorTxs==true', adds the tx to tc.currBatch.L1CoordinatorTxs.
330func (tc *TestContext) calculateIdxForL1Txs(isCoordinatorTxs bool, txs []L1Tx) error {
331 // for each batch.L1CoordinatorTxs of the queues[ToForgeNum], calculate the Idx
332 for i := 0; i < len(txs); i++ {
333 tx := txs[i]
334 if tx.L1Tx.Type == common.TxTypeCreateAccountDeposit || tx.L1Tx.Type == common.TxTypeCreateAccountDepositTransfer {
335 if tc.Users[tx.fromIdxName].Accounts[tx.L1Tx.TokenID] != nil { // if account already exists, return error
336 return fmt.Errorf("Can not create same account twice (same User & same TokenID) (this is a design property of Transakcio)")
337 }
338 tc.Users[tx.fromIdxName].Accounts[tx.L1Tx.TokenID] = &Account{
339 Idx: common.Idx(tc.idx),
340 Nonce: common.Nonce(0),
341 }
342 tc.l1CreatedAccounts[idxTokenIDToString(tx.fromIdxName, tx.L1Tx.TokenID)] = tc.Users[tx.fromIdxName].Accounts[tx.L1Tx.TokenID]
343 tc.idx++
344 }
345 if isCoordinatorTxs {
346 tc.currBatch.L1CoordinatorTxs = append(tc.currBatch.L1CoordinatorTxs, tx.L1Tx)
347 }
348 }
349 return nil
350}
351
352// setIdxs sets the Idxs to the transactions of the tc.currBatch
353func (tc *TestContext) setIdxs() error {
354 // once Idxs are calculated, update transactions to use the new Idxs
355 for i := 0; i < len(tc.currBatch.testL2Txs); i++ {
356 testTx := &tc.currBatch.testL2Txs[i]
357
358 if tc.Users[testTx.fromIdxName].Accounts[testTx.tokenID] == nil {
359 return fmt.Errorf("Line %d: %s from User %s for TokenID %d while account not created yet", testTx.lineNum, testTx.L2Tx.Type, testTx.fromIdxName, testTx.tokenID)
360 }
361 if testTx.L2Tx.Type == common.TxTypeTransfer {
362 if _, ok := tc.l1CreatedAccounts[idxTokenIDToString(testTx.toIdxName, testTx.tokenID)]; !ok {
363 return fmt.Errorf("Line %d: Can not create Transfer for a non existing account. Batch %d, ToIdx name: %s, TokenID: %d", testTx.lineNum, tc.currBatchNum, testTx.toIdxName, testTx.tokenID)
364 }
365 }
366 tc.Users[testTx.fromIdxName].Accounts[testTx.tokenID].Nonce++
367 testTx.L2Tx.Nonce = tc.Users[testTx.fromIdxName].Accounts[testTx.tokenID].Nonce
368
369 // set real Idx
370 testTx.L2Tx.FromIdx = tc.Users[testTx.fromIdxName].Accounts[testTx.tokenID].Idx
371 if testTx.L2Tx.Type == common.TxTypeTransfer {
372 testTx.L2Tx.ToIdx = tc.Users[testTx.toIdxName].Accounts[testTx.tokenID].Idx
373 }
374 nTx, err := common.NewL2Tx(&testTx.L2Tx)
375 if err != nil {
376 return err
377 }
378 testTx.L2Tx = *nTx
379
380 tc.currBatch.L2Txs = append(tc.currBatch.L2Txs, testTx.L2Tx)
381 }
382
383 tc.currBlock.Batches = append(tc.currBlock.Batches, tc.currBatch)
384 tc.currBatchNum++
385 tc.currBatch = BatchData{}
386
387 return nil
388}
389
390// addToL1Queue adds the L1Tx into the queue that is open and has space
391func (tc *TestContext) addToL1Queue(tx L1Tx) {
392 if len(tc.queues[tc.openToForge]) >= tc.rollupConstMaxL1UserTx {
393 // if current OpenToForge queue reached its Max, move into a
394 // new queue
395 tc.openToForge++
396 newQueue := []L1Tx{}
397 tc.queues = append(tc.queues, newQueue)
398 }
399 tc.queues[tc.openToForge] = append(tc.queues[tc.openToForge], tx)
400}
401
402func (tc *TestContext) checkIfAccountExists(tf string, inst instruction) error {
403 if tc.Users[tf].Accounts[inst.tokenID] == nil {
404 return fmt.Errorf("%s at User: %s, for TokenID: %d, while account not created yet", inst.typ, tf, inst.tokenID)
405 }
406 return nil
407}
408func (tc *TestContext) checkIfTokenIsRegistered(inst instruction) error {
409 if inst.tokenID > tc.lastRegisteredTokenID {
410 return fmt.Errorf("Can not process %s: TokenID %d not registered, last registered TokenID: %d", inst.typ, inst.tokenID, tc.lastRegisteredTokenID)
411 }
412 return nil
413}
414
415// GeneratePoolL2Txs returns an array of common.PoolL2Tx from a given set. It
416// uses the accounts (keys & nonces) of the TestContext.
417func (tc *TestContext) GeneratePoolL2Txs(set string) ([]common.PoolL2Tx, error) {
418 parser := newParser(strings.NewReader(set))
419 parsedSet, err := parser.parse()
420 if err != nil {
421 return nil, err
422 }
423
424 tc.Instructions = parsedSet.instructions
425 tc.accountsNames = parsedSet.accounts
426
427 tc.generateKeys(tc.accountsNames)
428
429 txs := []common.PoolL2Tx{}
430 for _, inst := range tc.Instructions {
431 switch inst.typ {
432 case common.TxTypeTransfer:
433 if err := tc.checkIfAccountExists(inst.from, inst); err != nil {
434 log.Error(err)
435 return nil, fmt.Errorf("Line %d: %s", inst.lineNum, err.Error())
436 }
437 if err := tc.checkIfAccountExists(inst.to, inst); err != nil {
438 log.Error(err)
439 return nil, fmt.Errorf("Line %d: %s", inst.lineNum, err.Error())
440 }
441 tc.Users[inst.from].Accounts[inst.tokenID].Nonce++
442 // if account of receiver does not exist, don't use
443 // ToIdx, and use only ToEthAddr & ToBJJ
444 tx := common.PoolL2Tx{
445 FromIdx: tc.Users[inst.from].Accounts[inst.tokenID].Idx,
446 ToIdx: tc.Users[inst.to].Accounts[inst.tokenID].Idx,
447 ToEthAddr: tc.Users[inst.to].Addr,
448 ToBJJ: tc.Users[inst.to].BJJ.Public(),
449 TokenID: inst.tokenID,
450 Amount: big.NewInt(int64(inst.amount)),
451 Fee: common.FeeSelector(inst.fee),
452 Nonce: tc.Users[inst.from].Accounts[inst.tokenID].Nonce,
453 State: common.PoolL2TxStatePending,
454 Timestamp: time.Now(),
455 RqToEthAddr: common.EmptyAddr,
456 RqToBJJ: nil,
457 Type: common.TxTypeTransfer,
458 }
459 nTx, err := common.NewPoolL2Tx(&tx)
460 if err != nil {
461 return nil, fmt.Errorf("Line %d: %s", inst.lineNum, err.Error())
462 }
463 tx = *nTx
464 // perform signature and set it to tx.Signature
465 toSign, err := tx.HashToSign()
466 if err != nil {
467 return nil, fmt.Errorf("Line %d: %s", inst.lineNum, err.Error())
468 }
469 sig := tc.Users[inst.to].BJJ.SignPoseidon(toSign)
470 tx.Signature = sig
471
472 txs = append(txs, tx)
473 case common.TxTypeExit:
474 tc.Users[inst.from].Accounts[inst.tokenID].Nonce++
475 tx := common.PoolL2Tx{
476 FromIdx: tc.Users[inst.from].Accounts[inst.tokenID].Idx,
477 ToIdx: common.Idx(1), // as is an Exit
478 TokenID: inst.tokenID,
479 Amount: big.NewInt(int64(inst.amount)),
480 Nonce: tc.Users[inst.from].Accounts[inst.tokenID].Nonce,
481 Type: common.TxTypeExit,
482 }
483 txs = append(txs, tx)
484 default:
485 return nil, fmt.Errorf("Line %d: instruction type unrecognized: %s", inst.lineNum, inst.typ)
486 }
487 }
488
489 return txs, nil
490}
491
492// generateKeys generates BabyJubJub & Address keys for the given list of
493// account names in a deterministic way. This means, that for the same given
494// 'accNames' in a certain order, the keys will be always the same.
495func (tc *TestContext) generateKeys(accNames []string) {
496 for i := 1; i < len(accNames)+1; i++ {
497 if _, ok := tc.Users[accNames[i-1]]; ok {
498 // account already created
499 continue
500 }
501 // babyjubjub key
502 var sk babyjub.PrivateKey
503 copy(sk[:], []byte(strconv.Itoa(i))) // only for testing
504
505 // eth address
506 var key ecdsa.PrivateKey
507 key.D = big.NewInt(int64(i)) // only for testing
508 key.PublicKey.X, key.PublicKey.Y = ethCrypto.S256().ScalarBaseMult(key.D.Bytes())
509 key.Curve = ethCrypto.S256()
510 addr := ethCrypto.PubkeyToAddress(key.PublicKey)
511
512 u := User{
513 BJJ: &sk,
514 Addr: addr,
515 Accounts: make(map[common.TokenID]*Account),
516 }
517 tc.Users[accNames[i-1]] = &u
518 }
519}