You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

357 lines
12 KiB

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