hermez-node/api/txspool_test.go
api/txspool_test.goRaw
1package api
2
3import (
4 "bytes"
5 "crypto/ecdsa"
6 "encoding/binary"
7 "encoding/hex"
8 "encoding/json"
9 "math/big"
10 "testing"
11 "time"
12
13 ethCrypto "github.com/ethereum/go-ethereum/crypto"
14 "github.com/hermeznetwork/hermez-node/common"
15 "github.com/hermeznetwork/hermez-node/db/historydb"
16 "github.com/iden3/go-iden3-crypto/babyjub"
17 "github.com/stretchr/testify/assert"
18 "github.com/stretchr/testify/require"
19)
20
21// testPoolTxReceive is a struct to be used to assert the response
22// of GET /transactions-pool/:id
23type testPoolTxReceive struct {
24 TxID common.TxID `json:"id"`
25 Type common.TxType `json:"type"`
26 FromIdx string `json:"fromAccountIndex"`
27 FromEthAddr *string `json:"fromHezEthereumAddress"`
28 FromBJJ *string `json:"fromBJJ"`
29 ToIdx *string `json:"toAccountIndex"`
30 ToEthAddr *string `json:"toHezEthereumAddress"`
31 ToBJJ *string `json:"toBjj"`
32 Amount string `json:"amount"`
33 Fee common.FeeSelector `json:"fee"`
34 Nonce common.Nonce `json:"nonce"`
35 State common.PoolL2TxState `json:"state"`
36 Signature babyjub.SignatureComp `json:"signature"`
37 RqFromIdx *string `json:"requestFromAccountIndex"`
38 RqToIdx *string `json:"requestToAccountIndex"`
39 RqToEthAddr *string `json:"requestToHezEthereumAddress"`
40 RqToBJJ *string `json:"requestToBJJ"`
41 RqTokenID *common.TokenID `json:"requestTokenId"`
42 RqAmount *string `json:"requestAmount"`
43 RqFee *common.FeeSelector `json:"requestFee"`
44 RqNonce *common.Nonce `json:"requestNonce"`
45 BatchNum *common.BatchNum `json:"batchNum"`
46 Timestamp time.Time `json:"timestamp"`
47 Token historydb.TokenWithUSD `json:"token"`
48}
49
50// testPoolTxSend is a struct to be used as a JSON body
51// when testing POST /transactions-pool
52type testPoolTxSend struct {
53 TxID common.TxID `json:"id" binding:"required"`
54 Type common.TxType `json:"type" binding:"required"`
55 TokenID common.TokenID `json:"tokenId"`
56 FromIdx string `json:"fromAccountIndex" binding:"required"`
57 ToIdx *string `json:"toAccountIndex"`
58 ToEthAddr *string `json:"toHezEthereumAddress"`
59 ToBJJ *string `json:"toBjj"`
60 Amount string `json:"amount" binding:"required"`
61 Fee common.FeeSelector `json:"fee"`
62 Nonce common.Nonce `json:"nonce"`
63 Signature babyjub.SignatureComp `json:"signature" binding:"required"`
64 RqFromIdx *string `json:"requestFromAccountIndex"`
65 RqToIdx *string `json:"requestToAccountIndex"`
66 RqToEthAddr *string `json:"requestToHezEthereumAddress"`
67 RqToBJJ *string `json:"requestToBjj"`
68 RqTokenID *common.TokenID `json:"requestTokenId"`
69 RqAmount *string `json:"requestAmount"`
70 RqFee *common.FeeSelector `json:"requestFee"`
71 RqNonce *common.Nonce `json:"requestNonce"`
72}
73
74func genTestPoolTxs(
75 poolTxs []common.PoolL2Tx,
76 tokens []historydb.TokenWithUSD,
77 accs []common.Account,
78) (poolTxsToSend []testPoolTxSend, poolTxsToReceive []testPoolTxReceive) {
79 poolTxsToSend = []testPoolTxSend{}
80 poolTxsToReceive = []testPoolTxReceive{}
81 for _, poolTx := range poolTxs {
82 // common.PoolL2Tx ==> testPoolTxSend
83 token := getTokenByID(poolTx.TokenID, tokens)
84 genSendTx := testPoolTxSend{
85 TxID: poolTx.TxID,
86 Type: poolTx.Type,
87 TokenID: poolTx.TokenID,
88 FromIdx: idxToHez(poolTx.FromIdx, token.Symbol),
89 Amount: poolTx.Amount.String(),
90 Fee: poolTx.Fee,
91 Nonce: poolTx.Nonce,
92 Signature: poolTx.Signature,
93 RqFee: &poolTx.RqFee,
94 RqNonce: &poolTx.RqNonce,
95 }
96 // common.PoolL2Tx ==> testPoolTxReceive
97 genReceiveTx := testPoolTxReceive{
98 TxID: poolTx.TxID,
99 Type: poolTx.Type,
100 FromIdx: idxToHez(poolTx.FromIdx, token.Symbol),
101 Amount: poolTx.Amount.String(),
102 Fee: poolTx.Fee,
103 Nonce: poolTx.Nonce,
104 State: poolTx.State,
105 Signature: poolTx.Signature,
106 Timestamp: poolTx.Timestamp,
107 // BatchNum: poolTx.BatchNum,
108 RqFee: &poolTx.RqFee,
109 RqNonce: &poolTx.RqNonce,
110 Token: token,
111 }
112 fromAcc := getAccountByIdx(poolTx.FromIdx, accs)
113 fromAddr := ethAddrToHez(fromAcc.EthAddr)
114 genReceiveTx.FromEthAddr = &fromAddr
115 fromBjj := bjjToString(fromAcc.BJJ)
116 genReceiveTx.FromBJJ = &fromBjj
117 if poolTx.ToIdx != 0 {
118 toIdx := idxToHez(poolTx.ToIdx, token.Symbol)
119 genSendTx.ToIdx = &toIdx
120 genReceiveTx.ToIdx = &toIdx
121 }
122 if poolTx.ToEthAddr != common.EmptyAddr {
123 toEth := ethAddrToHez(poolTx.ToEthAddr)
124 genSendTx.ToEthAddr = &toEth
125 genReceiveTx.ToEthAddr = &toEth
126 } else if poolTx.ToIdx > 255 {
127 acc := getAccountByIdx(poolTx.ToIdx, accs)
128 addr := ethAddrToHez(acc.EthAddr)
129 genReceiveTx.ToEthAddr = &addr
130 }
131 if poolTx.ToBJJ != common.EmptyBJJComp {
132 toBJJ := bjjToString(poolTx.ToBJJ)
133 genSendTx.ToBJJ = &toBJJ
134 genReceiveTx.ToBJJ = &toBJJ
135 } else if poolTx.ToIdx > 255 {
136 acc := getAccountByIdx(poolTx.ToIdx, accs)
137 bjj := bjjToString(acc.BJJ)
138 genReceiveTx.ToBJJ = &bjj
139 }
140 if poolTx.RqFromIdx != 0 {
141 rqFromIdx := idxToHez(poolTx.RqFromIdx, token.Symbol)
142 genSendTx.RqFromIdx = &rqFromIdx
143 genReceiveTx.RqFromIdx = &rqFromIdx
144 genSendTx.RqTokenID = &token.TokenID
145 genReceiveTx.RqTokenID = &token.TokenID
146 rqAmount := poolTx.RqAmount.String()
147 genSendTx.RqAmount = &rqAmount
148 genReceiveTx.RqAmount = &rqAmount
149
150 if poolTx.RqToIdx != 0 {
151 rqToIdx := idxToHez(poolTx.RqToIdx, token.Symbol)
152 genSendTx.RqToIdx = &rqToIdx
153 genReceiveTx.RqToIdx = &rqToIdx
154 }
155 if poolTx.RqToEthAddr != common.EmptyAddr {
156 rqToEth := ethAddrToHez(poolTx.RqToEthAddr)
157 genSendTx.RqToEthAddr = &rqToEth
158 genReceiveTx.RqToEthAddr = &rqToEth
159 }
160 if poolTx.RqToBJJ != common.EmptyBJJComp {
161 rqToBJJ := bjjToString(poolTx.RqToBJJ)
162 genSendTx.RqToBJJ = &rqToBJJ
163 genReceiveTx.RqToBJJ = &rqToBJJ
164 }
165 }
166
167 poolTxsToSend = append(poolTxsToSend, genSendTx)
168 poolTxsToReceive = append(poolTxsToReceive, genReceiveTx)
169 }
170 return poolTxsToSend, poolTxsToReceive
171}
172
173func TestPoolTxs(t *testing.T) {
174 // POST
175 endpoint := apiURL + "transactions-pool"
176 fetchedTxID := common.TxID{}
177 for _, tx := range tc.poolTxsToSend {
178 jsonTxBytes, err := json.Marshal(tx)
179 require.NoError(t, err)
180 jsonTxReader := bytes.NewReader(jsonTxBytes)
181 require.NoError(
182 t, doGoodReq(
183 "POST",
184 endpoint,
185 jsonTxReader, &fetchedTxID,
186 ),
187 )
188 assert.Equal(t, tx.TxID, fetchedTxID)
189 }
190 // 400
191 // Wrong fee
192 badTx := tc.poolTxsToSend[0]
193 badTx.Amount = "99950000000000000"
194 badTx.Fee = 255
195 jsonTxBytes, err := json.Marshal(badTx)
196 require.NoError(t, err)
197 jsonTxReader := bytes.NewReader(jsonTxBytes)
198 err = doBadReq("POST", endpoint, jsonTxReader, 400)
199 require.NoError(t, err)
200 // Wrong signature
201 badTx = tc.poolTxsToSend[0]
202 badTx.FromIdx = "hez:foo:1000"
203 jsonTxBytes, err = json.Marshal(badTx)
204 require.NoError(t, err)
205 jsonTxReader = bytes.NewReader(jsonTxBytes)
206 err = doBadReq("POST", endpoint, jsonTxReader, 400)
207 require.NoError(t, err)
208 // Wrong to
209 badTx = tc.poolTxsToSend[0]
210 ethAddr := "hez:0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"
211 badTx.ToEthAddr = ðAddr
212 badTx.ToIdx = nil
213 jsonTxBytes, err = json.Marshal(badTx)
214 require.NoError(t, err)
215 jsonTxReader = bytes.NewReader(jsonTxBytes)
216 err = doBadReq("POST", endpoint, jsonTxReader, 400)
217 require.NoError(t, err)
218 // Wrong rq
219 badTx = tc.poolTxsToSend[0]
220 rqFromIdx := "hez:foo:30"
221 badTx.RqFromIdx = &rqFromIdx
222 jsonTxBytes, err = json.Marshal(badTx)
223 require.NoError(t, err)
224 jsonTxReader = bytes.NewReader(jsonTxBytes)
225 err = doBadReq("POST", endpoint, jsonTxReader, 400)
226 require.NoError(t, err)
227 // GET
228 endpoint += "/"
229 for _, tx := range tc.poolTxsToReceive {
230 fetchedTx := testPoolTxReceive{}
231 require.NoError(
232 t, doGoodReq(
233 "GET",
234 endpoint+tx.TxID.String(),
235 nil, &fetchedTx,
236 ),
237 )
238 assertPoolTx(t, tx, fetchedTx)
239 }
240 // 400, due invalid TxID
241 err = doBadReq("GET", endpoint+"0xG2241b6f2b1dd772dba391f4a1a3407c7c21f598d86e2585a14e616fb4a255f823", nil, 400)
242 require.NoError(t, err)
243 // 404, due inexistent TxID in DB
244 err = doBadReq("GET", endpoint+"0x02241b6f2b1dd772dba391f4a1a3407c7c21f598d86e2585a14e616fb4a255f823", nil, 404)
245 require.NoError(t, err)
246}
247
248func assertPoolTx(t *testing.T, expected, actual testPoolTxReceive) {
249 // state should be pending
250 assert.Equal(t, common.PoolL2TxStatePending, actual.State)
251 expected.State = actual.State
252 actual.Token.ItemID = 0
253 // timestamp should be very close to now
254 assert.Less(t, time.Now().UTC().Unix()-3, actual.Timestamp.Unix())
255 expected.Timestamp = actual.Timestamp
256 // token timestamp
257 if expected.Token.USDUpdate == nil {
258 assert.Equal(t, expected.Token.USDUpdate, actual.Token.USDUpdate)
259 } else {
260 assert.Equal(t, expected.Token.USDUpdate.Unix(), actual.Token.USDUpdate.Unix())
261 expected.Token.USDUpdate = actual.Token.USDUpdate
262 }
263 assert.Equal(t, expected, actual)
264}
265
266// TestAllTosNull test that the API doesn't accept txs with all the TOs set to null (to eth, to bjj, to idx)
267func TestAllTosNull(t *testing.T) {
268 // Generate account:
269 // Ethereum private key
270 var key ecdsa.PrivateKey
271 key.D = big.NewInt(int64(4444)) // only for testing
272 key.PublicKey.X, key.PublicKey.Y = ethCrypto.S256().ScalarBaseMult(key.D.Bytes())
273 key.Curve = ethCrypto.S256()
274 addr := ethCrypto.PubkeyToAddress(key.PublicKey)
275 // BJJ private key
276 var sk babyjub.PrivateKey
277 var iBytes [8]byte
278 binary.LittleEndian.PutUint64(iBytes[:], 4444)
279 copy(sk[:], iBytes[:]) // only for testing
280 account := common.Account{
281 Idx: 4444,
282 TokenID: 0,
283 BatchNum: 1,
284 BJJ: sk.Public().Compress(),
285 EthAddr: addr,
286 Nonce: 0,
287 Balance: big.NewInt(1000000),
288 }
289 // Add account to history DB (required to verify signature)
290 err := api.h.AddAccounts([]common.Account{account})
291 assert.NoError(t, err)
292 // Genrate tx with all tos set to nil (to eth, to bjj, to idx)
293 tx := common.PoolL2Tx{
294 FromIdx: account.Idx,
295 TokenID: account.TokenID,
296 Amount: big.NewInt(1000),
297 Fee: 200,
298 Nonce: 0,
299 }
300 // Set idx and type manually, and check that the function doesn't allow it
301 _, err = common.NewPoolL2Tx(&tx)
302 assert.Error(t, err)
303 tx.Type = common.TxTypeTransfer
304 var txID common.TxID
305 txIDRaw, err := hex.DecodeString("02e66e24f7f25272906647c8fd1d7fe8acf3cf3e9b38ffc9f94bbb5090dc275073")
306 assert.NoError(t, err)
307 copy(txID[:], txIDRaw)
308 tx.TxID = txID
309 // Sign tx
310 toSign, err := tx.HashToSign(0)
311 assert.NoError(t, err)
312 sig := sk.SignPoseidon(toSign)
313 tx.Signature = sig.Compress()
314 // Transform common.PoolL2Tx ==> testPoolTxSend
315 txToSend := testPoolTxSend{
316 TxID: tx.TxID,
317 Type: tx.Type,
318 TokenID: tx.TokenID,
319 FromIdx: idxToHez(tx.FromIdx, "ETH"),
320 Amount: tx.Amount.String(),
321 Fee: tx.Fee,
322 Nonce: tx.Nonce,
323 Signature: tx.Signature,
324 }
325 // Send tx to the API
326 jsonTxBytes, err := json.Marshal(txToSend)
327 require.NoError(t, err)
328 jsonTxReader := bytes.NewReader(jsonTxBytes)
329 err = doBadReq("POST", apiURL+"transactions-pool", jsonTxReader, 400)
330 require.NoError(t, err)
331 // Clean historyDB: the added account shouldn't be there for other tests
332 _, err = api.h.DB().DB.Exec("delete from account where idx = 4444")
333 assert.NoError(t, err)
334}