hermez-node/db/l2db/l2db.go
db/l2db/l2db.goRaw
1package l2db
2
3import (
4 "errors"
5 "math/big"
6 "time"
7
8 ethCommon "github.com/ethereum/go-ethereum/common"
9 "github.com/hermeznetwork/hermez-node/common"
10 "github.com/iden3/go-iden3-crypto/babyjub"
11 "github.com/jmoiron/sqlx"
12
13 //nolint:errcheck // driver for postgres DB
14 _ "github.com/lib/pq"
15 "github.com/russross/meddler"
16)
17
18// TODO(Edu): Check DB consistency while there's concurrent use from Coordinator/TxSelector & API
19
20// L2DB stores L2 txs and authorization registers received by the coordinator and keeps them until they are no longer relevant
21// due to them being forged or invalid after a safety period
22type L2DB struct {
23 db *sqlx.DB
24 safetyPeriod common.BatchNum
25 ttl time.Duration
26 maxTxs uint32
27}
28
29// NewL2DB creates a L2DB.
30// To create it, it's needed db connection, safety period expressed in batches,
31// maxTxs that the DB should have and TTL (time to live) for pending txs.
32func NewL2DB(db *sqlx.DB, safetyPeriod common.BatchNum, maxTxs uint32, TTL time.Duration) *L2DB {
33 return &L2DB{
34 db: db,
35 safetyPeriod: safetyPeriod,
36 ttl: TTL,
37 maxTxs: maxTxs,
38 }
39}
40
41// DB returns a pointer to the L2DB.db. This method should be used only for
42// internal testing purposes.
43func (l2db *L2DB) DB() *sqlx.DB {
44 return l2db.db
45}
46
47// AddAccountCreationAuth inserts an account creation authorization into the DB
48func (l2db *L2DB) AddAccountCreationAuth(auth *common.AccountCreationAuth) error {
49 return meddler.Insert(l2db.db, "account_creation_auth", auth)
50}
51
52// GetAccountCreationAuth returns an account creation authorization into the DB
53func (l2db *L2DB) GetAccountCreationAuth(addr *ethCommon.Address) (*common.AccountCreationAuth, error) {
54 if addr == nil {
55 return nil, errors.New("addr cannot be nil")
56 }
57 auth := new(common.AccountCreationAuth)
58 return auth, meddler.QueryRow(
59 l2db.db, auth,
60 "SELECT * FROM account_creation_auth WHERE eth_addr = $1;",
61 addr,
62 )
63}
64
65// AddTxTest inserts a tx into the L2DB. This is useful for test purposes,
66// but in production txs will only be inserted through the API (method TBD)
67func (l2db *L2DB) AddTxTest(tx *common.PoolL2Tx) error {
68 type withouUSD struct {
69 TxID common.TxID `meddler:"tx_id"`
70 FromIdx common.Idx `meddler:"from_idx"`
71 ToIdx *common.Idx `meddler:"to_idx"`
72 ToEthAddr *ethCommon.Address `meddler:"to_eth_addr"`
73 ToBJJ *babyjub.PublicKey `meddler:"to_bjj"`
74 TokenID common.TokenID `meddler:"token_id"`
75 Amount *big.Int `meddler:"amount,bigint"`
76 AmountFloat float64 `meddler:"amount_f"`
77 Fee common.FeeSelector `meddler:"fee"`
78 Nonce common.Nonce `meddler:"nonce"`
79 State common.PoolL2TxState `meddler:"state"`
80 Signature *babyjub.Signature `meddler:"signature"`
81 Timestamp time.Time `meddler:"timestamp,utctime"`
82 BatchNum *common.BatchNum `meddler:"batch_num"`
83 RqFromIdx *common.Idx `meddler:"rq_from_idx"`
84 RqToIdx *common.Idx `meddler:"rq_to_idx"`
85 RqToEthAddr *ethCommon.Address `meddler:"rq_to_eth_addr"`
86 RqToBJJ *babyjub.PublicKey `meddler:"rq_to_bjj"`
87 RqTokenID *common.TokenID `meddler:"rq_token_id"`
88 RqAmount *big.Int `meddler:"rq_amount,bigintnull"`
89 RqFee *common.FeeSelector `meddler:"rq_fee"`
90 RqNonce *uint64 `meddler:"rq_nonce"`
91 Type common.TxType `meddler:"tx_type"`
92 }
93 return meddler.Insert(l2db.db, "tx_pool", &withouUSD{
94 TxID: tx.TxID,
95 FromIdx: tx.FromIdx,
96 ToIdx: tx.ToIdx,
97 ToEthAddr: tx.ToEthAddr,
98 ToBJJ: tx.ToBJJ,
99 TokenID: tx.TokenID,
100 Amount: tx.Amount,
101 AmountFloat: tx.AmountFloat,
102 Fee: tx.Fee,
103 Nonce: tx.Nonce,
104 State: tx.State,
105 Signature: tx.Signature,
106 Timestamp: tx.Timestamp,
107 BatchNum: tx.BatchNum,
108 RqFromIdx: tx.RqFromIdx,
109 RqToIdx: tx.RqToIdx,
110 RqToEthAddr: tx.RqToEthAddr,
111 RqToBJJ: tx.RqToBJJ,
112 RqTokenID: tx.RqTokenID,
113 RqAmount: tx.RqAmount,
114 RqFee: tx.RqFee,
115 RqNonce: tx.RqNonce,
116 Type: tx.Type,
117 })
118}
119
120// selectPoolTx select part of queries to get common.PoolL2Tx
121const selectPoolTx = `SELECT tx_pool.*, token.usd * tx_pool.amount_f AS value_usd,
122fee_percentage(tx_pool.fee::NUMERIC) * token.usd * tx_pool.amount_f AS fee_usd, token.usd_update
123FROM tx_pool INNER JOIN token ON tx_pool.token_id = token.token_id `
124
125// GetTx return the specified Tx
126func (l2db *L2DB) GetTx(txID common.TxID) (*common.PoolL2Tx, error) {
127 tx := new(common.PoolL2Tx)
128 return tx, meddler.QueryRow(
129 l2db.db, tx,
130 selectPoolTx+"WHERE tx_id = $1;",
131 txID,
132 )
133}
134
135// GetPendingTxs return all the pending txs of the L2DB, that have a non NULL AbsoluteFee
136func (l2db *L2DB) GetPendingTxs() ([]*common.PoolL2Tx, error) {
137 var txs []*common.PoolL2Tx
138 err := meddler.QueryAll(
139 l2db.db, &txs,
140 selectPoolTx+"WHERE state = $1 AND token.usd IS NOT NULL",
141 common.PoolL2TxStatePending,
142 )
143 return txs, err
144}
145
146// StartForging updates the state of the transactions that will begin the forging process.
147// The state of the txs referenced by txIDs will be changed from Pending -> Forging
148func (l2db *L2DB) StartForging(txIDs []common.TxID, batchNum common.BatchNum) error {
149 query, args, err := sqlx.In(
150 `UPDATE tx_pool
151 SET state = ?, batch_num = ?
152 WHERE state = ? AND tx_id IN (?);`,
153 common.PoolL2TxStateForging,
154 batchNum,
155 common.PoolL2TxStatePending,
156 txIDs,
157 )
158 if err != nil {
159 return err
160 }
161 query = l2db.db.Rebind(query)
162 _, err = l2db.db.Exec(query, args...)
163 return err
164}
165
166// DoneForging updates the state of the transactions that have been forged
167// so the state of the txs referenced by txIDs will be changed from Forging -> Forged
168func (l2db *L2DB) DoneForging(txIDs []common.TxID, batchNum common.BatchNum) error {
169 query, args, err := sqlx.In(
170 `UPDATE tx_pool
171 SET state = ?, batch_num = ?
172 WHERE state = ? AND tx_id IN (?);`,
173 common.PoolL2TxStateForged,
174 batchNum,
175 common.PoolL2TxStateForging,
176 txIDs,
177 )
178 if err != nil {
179 return err
180 }
181 query = l2db.db.Rebind(query)
182 _, err = l2db.db.Exec(query, args...)
183 return err
184}
185
186// InvalidateTxs updates the state of the transactions that are invalid.
187// The state of the txs referenced by txIDs will be changed from * -> Invalid
188func (l2db *L2DB) InvalidateTxs(txIDs []common.TxID, batchNum common.BatchNum) error {
189 query, args, err := sqlx.In(
190 `UPDATE tx_pool
191 SET state = ?, batch_num = ?
192 WHERE tx_id IN (?);`,
193 common.PoolL2TxStateInvalid,
194 batchNum,
195 txIDs,
196 )
197 if err != nil {
198 return err
199 }
200 query = l2db.db.Rebind(query)
201 _, err = l2db.db.Exec(query, args...)
202 return err
203}
204
205// CheckNonces invalidate txs with nonces that are smaller or equal than their respective accounts nonces.
206// The state of the affected txs will be changed from Pending -> Invalid
207func (l2db *L2DB) CheckNonces(updatedAccounts []common.Account, batchNum common.BatchNum) (err error) {
208 txn, err := l2db.db.Begin()
209 if err != nil {
210 return err
211 }
212 defer func() {
213 // Rollback the transaction if there was an error.
214 if err != nil {
215 err = txn.Rollback()
216 }
217 }()
218 for i := 0; i < len(updatedAccounts); i++ {
219 _, err = txn.Exec(
220 `UPDATE tx_pool
221 SET state = $1, batch_num = $2
222 WHERE state = $3 AND from_idx = $4 AND nonce <= $5;`,
223 common.PoolL2TxStateInvalid,
224 batchNum,
225 common.PoolL2TxStatePending,
226 updatedAccounts[i].Idx,
227 updatedAccounts[i].Nonce,
228 )
229 if err != nil {
230 return err
231 }
232 }
233 return txn.Commit()
234}
235
236// Reorg updates the state of txs that were updated in a batch that has been discarted due to a blockchain reorg.
237// The state of the affected txs can change form Forged -> Pending or from Invalid -> Pending
238func (l2db *L2DB) Reorg(lastValidBatch common.BatchNum) error {
239 _, err := l2db.db.Exec(
240 `UPDATE tx_pool SET batch_num = NULL, state = $1
241 WHERE (state = $2 OR state = $3) AND batch_num > $4`,
242 common.PoolL2TxStatePending,
243 common.PoolL2TxStateForged,
244 common.PoolL2TxStateInvalid,
245 lastValidBatch,
246 )
247 return err
248}
249
250// Purge deletes transactions that have been forged or marked as invalid for longer than the safety period
251// it also deletes txs that has been in the L2DB for longer than the ttl if maxTxs has been exceeded
252func (l2db *L2DB) Purge(currentBatchNum common.BatchNum) (err error) {
253 txn, err := l2db.db.Begin()
254 if err != nil {
255 return err
256 }
257 defer func() {
258 // Rollback the transaction if there was an error.
259 if err != nil {
260 err = txn.Rollback()
261 }
262 }()
263 // Delete pending txs that have been in the pool after the TTL if maxTxs is reached
264 now := time.Now().UTC().Unix()
265 _, err = txn.Exec(
266 `DELETE FROM tx_pool WHERE (SELECT count(*) FROM tx_pool) > $1 AND timestamp < $2`,
267 l2db.maxTxs,
268 time.Unix(now-int64(l2db.ttl.Seconds()), 0),
269 )
270 if err != nil {
271 return err
272 }
273 // Delete txs that have been marked as forged / invalid after the safety period
274 _, err = txn.Exec(
275 `DELETE FROM tx_pool
276 WHERE batch_num < $1 AND (state = $2 OR state = $3)`,
277 currentBatchNum-l2db.safetyPeriod,
278 common.PoolL2TxStateForged,
279 common.PoolL2TxStateInvalid,
280 )
281 if err != nil {
282 return err
283 }
284 return txn.Commit()
285}