arnaucube
arnaucube/hermez-nodePublic Self-hosted repository
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 = &ethAddr
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 nonexistent 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}