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 // set real Idx
274 testTx.L1Tx.FromIdx = tc.Users[testTx.fromIdxName].Accounts[testTx.L1Tx.TokenID].Idx
275 testTx.L1Tx.FromEthAddr = tc.Users[testTx.fromIdxName].Addr
276 testTx.L1Tx.FromBJJ = tc.Users[testTx.fromIdxName].BJJ.Public()
277 if testTx.toIdxName == "" {
278 testTx.L1Tx.ToIdx = common.Idx(0)
279 } else {
280 testTx.L1Tx.ToIdx = tc.Users[testTx.toIdxName].Accounts[testTx.L1Tx.TokenID].Idx
281 }
282 tc.currBlock.L1UserTxs = append(tc.currBlock.L1UserTxs, testTx.L1Tx)
283 }
284 if err = tc.setIdxs(); err != nil {
285 log.Error(err)
286 return nil, err
287 }
288 // advance batch
289 tc.toForgeNum++
290 if tc.toForgeNum == tc.openToForge {
291 tc.openToForge++
292 newQueue := []L1Tx{}
293 tc.queues = append(tc.queues, newQueue)
294 }
295 case typeNewBlock:
296 blocks = append(blocks, tc.currBlock)
297 tc.currBlock = BlockData{}
298 case typeRegisterToken:
299 newToken := common.Token{
300 TokenID: inst.tokenID,
301 EthBlockNum: int64(len(blocks)),
302 }
303 if inst.tokenID != tc.lastRegisteredTokenID+1 {
304 return nil, fmt.Errorf("Line %d: RegisterToken TokenID should be sequential, expected TokenID: %d, defined TokenID: %d", inst.lineNum, tc.lastRegisteredTokenID+1, inst.tokenID)
305 }
306 tc.lastRegisteredTokenID++
307 tc.currBlock.RegisteredTokens = append(tc.currBlock.RegisteredTokens, newToken)
308 default:
309 return nil, fmt.Errorf("Line %d: Unexpected type: %s", inst.lineNum, inst.typ)
310 }
311 }
312
313 return blocks, nil
314}
315
316// calculateIdxsForL1Txs calculates new Idx for new created accounts. If
317// 'isCoordinatorTxs==true', adds the tx to tc.currBatch.L1CoordinatorTxs.
318func (tc *TestContext) calculateIdxForL1Txs(isCoordinatorTxs bool, txs []L1Tx) error {
319 // for each batch.L1CoordinatorTxs of the queues[ToForgeNum], calculate the Idx
320 for i := 0; i < len(txs); i++ {
321 tx := txs[i]
322 if tx.L1Tx.Type == common.TxTypeCreateAccountDeposit || tx.L1Tx.Type == common.TxTypeCreateAccountDepositTransfer {
323 if tc.Users[tx.fromIdxName].Accounts[tx.L1Tx.TokenID] != nil { // if account already exists, return error
324 return fmt.Errorf("Can not create same account twice (same User & same TokenID) (this is a design property of Transakcio)")
325 }
326 tc.Users[tx.fromIdxName].Accounts[tx.L1Tx.TokenID] = &Account{
327 Idx: common.Idx(tc.idx),
328 Nonce: common.Nonce(0),
329 }
330 tc.l1CreatedAccounts[idxTokenIDToString(tx.fromIdxName, tx.L1Tx.TokenID)] = tc.Users[tx.fromIdxName].Accounts[tx.L1Tx.TokenID]
331 tc.idx++
332 }
333 if isCoordinatorTxs {
334 tc.currBatch.L1CoordinatorTxs = append(tc.currBatch.L1CoordinatorTxs, tx.L1Tx)
335 }
336 }
337 return nil
338}
339
340// setIdxs sets the Idxs to the transactions of the tc.currBatch
341func (tc *TestContext) setIdxs() error {
342 // once Idxs are calculated, update transactions to use the new Idxs
343 for i := 0; i < len(tc.currBatch.testL2Txs); i++ {
344 testTx := &tc.currBatch.testL2Txs[i]
345
346 if tc.Users[testTx.fromIdxName].Accounts[testTx.tokenID] == nil {
347 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)
348 }
349 if testTx.L2Tx.Type == common.TxTypeTransfer {
350 if _, ok := tc.l1CreatedAccounts[idxTokenIDToString(testTx.toIdxName, testTx.tokenID)]; !ok {
351 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)
352 }
353 }
354 tc.Users[testTx.fromIdxName].Accounts[testTx.tokenID].Nonce++
355 testTx.L2Tx.Nonce = tc.Users[testTx.fromIdxName].Accounts[testTx.tokenID].Nonce
356
357 // set real Idx
358 testTx.L2Tx.FromIdx = tc.Users[testTx.fromIdxName].Accounts[testTx.tokenID].Idx
359 if testTx.L2Tx.Type == common.TxTypeTransfer {
360 testTx.L2Tx.ToIdx = tc.Users[testTx.toIdxName].Accounts[testTx.tokenID].Idx
361 }
362 nTx, err := common.NewL2Tx(&testTx.L2Tx)
363 if err != nil {
364 return err
365 }
366 testTx.L2Tx = *nTx
367
368 tc.currBatch.L2Txs = append(tc.currBatch.L2Txs, testTx.L2Tx)
369 }
370
371 tc.currBlock.Batches = append(tc.currBlock.Batches, tc.currBatch)
372 tc.currBatchNum++
373 tc.currBatch = BatchData{}
374
375 return nil
376}
377
378// addToL1Queue adds the L1Tx into the queue that is open and has space
379func (tc *TestContext) addToL1Queue(tx L1Tx) {
380 if len(tc.queues[tc.openToForge]) >= tc.rollupConstMaxL1UserTx {
381 // if current OpenToForge queue reached its Max, move into a
382 // new queue
383 tc.openToForge++
384 newQueue := []L1Tx{}
385 tc.queues = append(tc.queues, newQueue)
386 }
387 tc.queues[tc.openToForge] = append(tc.queues[tc.openToForge], tx)
388}
389
390func (tc *TestContext) checkIfAccountExists(tf string, inst instruction) error {
391 if tc.Users[tf].Accounts[inst.tokenID] == nil {
392 return fmt.Errorf("%s at User: %s, for TokenID: %d, while account not created yet", inst.typ, tf, inst.tokenID)
393 }
394 return nil
395}
396func (tc *TestContext) checkIfTokenIsRegistered(inst instruction) error {
397 if inst.tokenID > tc.lastRegisteredTokenID {
398 return fmt.Errorf("Can not process %s: TokenID %d not registered, last registered TokenID: %d", inst.typ, inst.tokenID, tc.lastRegisteredTokenID)
399 }
400 return nil
401}
402
403// GeneratePoolL2Txs returns an array of common.PoolL2Tx from a given set. It
404// uses the accounts (keys & nonces) of the TestContext.
405func (tc *TestContext) GeneratePoolL2Txs(set string) ([]common.PoolL2Tx, error) {
406 parser := newParser(strings.NewReader(set))
407 parsedSet, err := parser.parse()
408 if err != nil {
409 return nil, err
410 }
411
412 tc.Instructions = parsedSet.instructions
413 tc.accountsNames = parsedSet.accounts
414
415 tc.generateKeys(tc.accountsNames)
416
417 txs := []common.PoolL2Tx{}
418 for _, inst := range tc.Instructions {
419 switch inst.typ {
420 case common.TxTypeTransfer:
421 if err := tc.checkIfAccountExists(inst.from, inst); err != nil {
422 log.Error(err)
423 return nil, fmt.Errorf("Line %d: %s", inst.lineNum, err.Error())
424 }
425 if err := tc.checkIfAccountExists(inst.to, inst); err != nil {
426 log.Error(err)
427 return nil, fmt.Errorf("Line %d: %s", inst.lineNum, err.Error())
428 }
429 tc.Users[inst.from].Accounts[inst.tokenID].Nonce++
430 // if account of receiver does not exist, don't use
431 // ToIdx, and use only ToEthAddr & ToBJJ
432 tx := common.PoolL2Tx{
433 FromIdx: tc.Users[inst.from].Accounts[inst.tokenID].Idx,
434 ToIdx: tc.Users[inst.to].Accounts[inst.tokenID].Idx,
435 ToEthAddr: tc.Users[inst.to].Addr,
436 ToBJJ: tc.Users[inst.to].BJJ.Public(),
437 TokenID: inst.tokenID,
438 Amount: big.NewInt(int64(inst.amount)),
439 Fee: common.FeeSelector(inst.fee),
440 Nonce: tc.Users[inst.from].Accounts[inst.tokenID].Nonce,
441 State: common.PoolL2TxStatePending,
442 Timestamp: time.Now(),
443 RqToEthAddr: common.EmptyAddr,
444 RqToBJJ: nil,
445 Type: common.TxTypeTransfer,
446 }
447 nTx, err := common.NewPoolL2Tx(&tx)
448 if err != nil {
449 return nil, fmt.Errorf("Line %d: %s", inst.lineNum, err.Error())
450 }
451 tx = *nTx
452 // perform signature and set it to tx.Signature
453 toSign, err := tx.HashToSign()
454 if err != nil {
455 return nil, fmt.Errorf("Line %d: %s", inst.lineNum, err.Error())
456 }
457 sig := tc.Users[inst.to].BJJ.SignPoseidon(toSign)
458 tx.Signature = sig
459
460 txs = append(txs, tx)
461 case common.TxTypeExit:
462 tc.Users[inst.from].Accounts[inst.tokenID].Nonce++
463 tx := common.PoolL2Tx{
464 FromIdx: tc.Users[inst.from].Accounts[inst.tokenID].Idx,
465 ToIdx: common.Idx(1), // as is an Exit
466 TokenID: inst.tokenID,
467 Amount: big.NewInt(int64(inst.amount)),
468 Nonce: tc.Users[inst.from].Accounts[inst.tokenID].Nonce,
469 Type: common.TxTypeExit,
470 }
471 txs = append(txs, tx)
472 default:
473 return nil, fmt.Errorf("Line %d: instruction type unrecognized: %s", inst.lineNum, inst.typ)
474 }
475 }
476
477 return txs, nil
478}
479
480// generateKeys generates BabyJubJub & Address keys for the given list of
481// account names in a deterministic way. This means, that for the same given
482// 'accNames' in a certain order, the keys will be always the same.
483func (tc *TestContext) generateKeys(accNames []string) {
484 for i := 1; i < len(accNames)+1; i++ {
485 if _, ok := tc.Users[accNames[i-1]]; ok {
486 // account already created
487 continue
488 }
489 // babyjubjub key
490 var sk babyjub.PrivateKey
491 copy(sk[:], []byte(strconv.Itoa(i))) // only for testing
492
493 // eth address
494 var key ecdsa.PrivateKey
495 key.D = big.NewInt(int64(i)) // only for testing
496 key.PublicKey.X, key.PublicKey.Y = ethCrypto.S256().ScalarBaseMult(key.D.Bytes())
497 key.Curve = ethCrypto.S256()
498 addr := ethCrypto.PubkeyToAddress(key.PublicKey)
499
500 u := User{
501 BJJ: &sk,
502 Addr: addr,
503 Accounts: make(map[common.TokenID]*Account),
504 }
505 tc.Users[accNames[i-1]] = &u
506 }
507}