diff --git a/api/api_types.go b/api/api_types.go index b1169db8d..1b8c89a21 100644 --- a/api/api_types.go +++ b/api/api_types.go @@ -292,9 +292,10 @@ type TransfersList struct { } type GenericTransactionWithInfo struct { - TxContent json.RawMessage `json:"tx"` - TxInfo indexertypes.Transaction `json:"txInfo"` - Signature types.HexBytes `json:"signature"` + TxContent json.RawMessage `json:"tx"` + TxInfo *indexertypes.Transaction `json:"txInfo"` + Signature types.HexBytes `json:"signature"` + Signer types.HexBytes `json:"signer"` } type ChainInfo struct { @@ -444,8 +445,9 @@ func CensusTypeToOrigin(ctype CensusTypeDescription) (models.CensusOrigin, []byt } type Block struct { - comettypes.Block `json:",inline"` - Hash types.HexBytes `json:"hash" ` + comettypes.Header `json:"header"` + Hash types.HexBytes `json:"hash" ` + TxCount int64 `json:"txCount"` } // BlockList is used to return a paginated list to the client diff --git a/api/chain.go b/api/chain.go index 47b86fbaa..1b4d5ca14 100644 --- a/api/chain.go +++ b/api/chain.go @@ -13,12 +13,9 @@ import ( "go.vocdoni.io/dvote/crypto/zk/circuit" "go.vocdoni.io/dvote/httprouter" "go.vocdoni.io/dvote/httprouter/apirest" - "go.vocdoni.io/dvote/types" "go.vocdoni.io/dvote/util" - "go.vocdoni.io/dvote/vochain" "go.vocdoni.io/dvote/vochain/genesis" "go.vocdoni.io/dvote/vochain/indexer" - "go.vocdoni.io/dvote/vochain/indexer/indexertypes" "go.vocdoni.io/dvote/vochain/state" ) @@ -167,7 +164,7 @@ func (a *API) enableChainHandlers() error { "/chain/blocks/{height}", "GET", apirest.MethodAccessTypePublic, - a.chainBlockHandler, + a.chainBlockByHeightHandler, ); err != nil { return err } @@ -690,14 +687,6 @@ func (a *API) chainTxHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) er if err != nil { return err } - stx, err := a.vocapp.GetTx(uint32(height), int32(index)) - if err != nil { - if errors.Is(err, vochain.ErrTransactionNotFound) { - return ErrTransactionNotFound - } - return ErrVochainGetTxFailed.WithErr(err) - } - ref, err := a.indexer.GetTransactionByHeightAndIndex(height, index) if err != nil { if errors.Is(err, indexer.ErrTransactionNotFound) { @@ -706,9 +695,10 @@ func (a *API) chainTxHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) er return ErrVochainGetTxFailed.WithErr(err) } tx := &GenericTransactionWithInfo{ - TxContent: []byte(protoFormat(stx.Tx)), - Signature: stx.Signature, - TxInfo: *ref, + TxContent: []byte(protoFormat(ref.RawTx)), + TxInfo: ref, + Signature: ref.Signature, + Signer: ref.Signer, } data, err := json.Marshal(tx) if err != nil { @@ -859,6 +849,8 @@ func (a *API) transactionList(params *TransactionParams) (*TransactionsList, err params.Page*params.Limit, params.Height, params.Type, + "", // TODO(gui): support new params + "", // ) if err != nil { return nil, ErrIndexerQueryFailed.WithErr(err) @@ -911,7 +903,7 @@ func (a *API) chainValidatorsHandler(_ *apirest.APIdata, ctx *httprouter.HTTPCon return ctx.Send(data, apirest.HTTPstatusOK) } -// chainBlockHandler +// chainBlockByHeightHandler // // @Summary Get block (by height) // @Description Returns the full block information at the given height @@ -921,23 +913,34 @@ func (a *API) chainValidatorsHandler(_ *apirest.APIdata, ctx *httprouter.HTTPCon // @Param height path int true "Block height" // @Success 200 {object} api.Block // @Router /chain/blocks/{height} [get] -func (a *API) chainBlockHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) error { +func (a *API) chainBlockByHeightHandler(_ *apirest.APIdata, ctx *httprouter.HTTPContext) error { height, err := strconv.ParseUint(ctx.URLParam(ParamHeight), 10, 64) if err != nil { return err } - tmblock := a.vocapp.GetBlockByHeight(int64(height)) - if tmblock == nil { - return ErrBlockNotFound + idxblock, err := a.indexer.BlockByHeight(int64(height)) + if err != nil { + if errors.Is(err, indexer.ErrBlockNotFound) { + return ErrBlockNotFound + } + return ErrBlockNotFound.WithErr(err) + } + txcount, err := a.indexer.CountTransactionsByHeight(int64(height)) + if err != nil { + return ErrIndexerQueryFailed.WithErr(err) } block := &Block{ - Block: comettypes.Block{ - Header: tmblock.Header, - Data: tmblock.Data, - Evidence: tmblock.Evidence, - LastCommit: tmblock.LastCommit, + Header: comettypes.Header{ + ChainID: idxblock.ChainID, + Height: idxblock.Height, + Time: idxblock.Time, + ProposerAddress: []byte(idxblock.ProposerAddress), + LastBlockID: comettypes.BlockID{ + Hash: []byte(idxblock.LastBlockHash), + }, }, - Hash: types.HexBytes(tmblock.Hash()), + Hash: idxblock.Hash, + TxCount: txcount, } data, err := json.Marshal(block) if err != nil { @@ -961,18 +964,29 @@ func (a *API) chainBlockByHashHandler(_ *apirest.APIdata, ctx *httprouter.HTTPCo if err != nil { return err } - tmblock := a.vocapp.GetBlockByHash(hash) - if tmblock == nil { - return ErrBlockNotFound + idxblock, err := a.indexer.BlockByHash(hash) + if err != nil { + if errors.Is(err, indexer.ErrBlockNotFound) { + return ErrBlockNotFound + } + return ErrBlockNotFound.WithErr(err) + } + txcount, err := a.indexer.CountTransactionsByHeight(idxblock.Height) + if err != nil { + return ErrIndexerQueryFailed.WithErr(err) } block := &Block{ - Block: comettypes.Block{ - Header: tmblock.Header, - Data: tmblock.Data, - Evidence: tmblock.Evidence, - LastCommit: tmblock.LastCommit, + Header: comettypes.Header{ + ChainID: idxblock.ChainID, + Height: idxblock.Height, + Time: idxblock.Time, + ProposerAddress: []byte(idxblock.ProposerAddress), + LastBlockID: comettypes.BlockID{ + Hash: []byte(idxblock.LastBlockHash), + }, }, - Hash: types.HexBytes(tmblock.Hash()), + Hash: idxblock.Hash, + TxCount: txcount, } data, err := json.Marshal(block) if err != nil { @@ -1015,39 +1029,7 @@ func (a *API) chainBlockListHandler(_ *apirest.APIdata, ctx *httprouter.HTTPCont // // Errors returned are always of type APIerror. func (a *API) sendBlockList(ctx *httprouter.HTTPContext, params *BlockParams) error { - // TODO: replace this by a.indexer.BlockList when it's available - blockList := func(limit, offset int, _, _, _ string) ([]*indexertypes.Block, uint64, error) { - if offset < 0 { - return nil, 0, fmt.Errorf("invalid value: offset cannot be %d", offset) - } - if limit <= 0 { - return nil, 0, fmt.Errorf("invalid value: limit cannot be %d", limit) - } - height := a.vocapp.Height() - total := uint64(height) - uint64(a.vocapp.Node.BlockStore().Base()) - start := height - uint32(params.Page*params.Limit) - end := start - uint32(params.Limit) - list := []*indexertypes.Block{} - for h := start; h > end; h-- { - tmblock := a.vocapp.GetBlockByHeight(int64(h)) - if tmblock == nil { - break - } - list = append(list, &indexertypes.Block{ - ChainID: tmblock.ChainID, - Height: tmblock.Height, - Time: tmblock.Time, - Hash: types.HexBytes(tmblock.Hash()), - ProposerAddress: tmblock.ProposerAddress.Bytes(), - LastBlockHash: tmblock.LastBlockID.Hash.Bytes(), - TxCount: int64(len(tmblock.Txs)), - }) - } - - return list, uint64(total), nil - } - - blocks, total, err := blockList( + blocks, total, err := a.indexer.BlockList( params.Limit, params.Page*params.Limit, params.ChainID, diff --git a/test/api_test.go b/test/api_test.go index 80e4f3699..267895636 100644 --- a/test/api_test.go +++ b/test/api_test.go @@ -461,6 +461,41 @@ func TestAPIAccountTokentxs(t *testing.T) { qt.Assert(t, gotAcct1.Balance, qt.Equals, initBalance+amountAcc2toAcct1-amountAcc1toAcct2-uint64(txBasePrice)) } +func TestAPIBlocks(t *testing.T) { + server := testcommon.APIserver{} + server.Start(t, + api.ChainHandler, + api.CensusHandler, + api.VoteHandler, + api.AccountHandler, + api.ElectionHandler, + api.WalletHandler, + ) + token1 := uuid.New() + c := testutil.NewTestHTTPclient(t, server.ListenAddr, &token1) + + // Block 1 + server.VochainAPP.AdvanceTestBlock() + waitUntilHeight(t, c, 1) + + // create a new account + initBalance := uint64(80) + _ = createAccount(t, c, server, initBalance) + + // Block 2 + server.VochainAPP.AdvanceTestBlock() + waitUntilHeight(t, c, 2) + + // check the txCount + resp, code := c.Request("GET", nil, "chain", "blocks", "1") + qt.Assert(t, code, qt.Equals, 200, qt.Commentf("response: %s", resp)) + + block := api.Block{} + err := json.Unmarshal(resp, &block) + qt.Assert(t, err, qt.IsNil) + qt.Assert(t, block.TxCount, qt.Equals, int64(1)) +} + func runAPIElectionCostWithParams(t *testing.T, electionParams electionprice.ElectionParameters, startBlock uint32, initialBalance, diff --git a/vochain/indexer/db/models.go b/vochain/indexer/db/models.go index db1778b1c..fab3d35ad 100644 --- a/vochain/indexer/db/models.go +++ b/vochain/indexer/db/models.go @@ -67,4 +67,6 @@ type Transaction struct { Type string RawTx []byte Signature []byte + Subtype string + Signer []byte } diff --git a/vochain/indexer/db/transactions.sql.go b/vochain/indexer/db/transactions.sql.go index a38c1a81a..c8628aebc 100644 --- a/vochain/indexer/db/transactions.sql.go +++ b/vochain/indexer/db/transactions.sql.go @@ -37,9 +37,9 @@ func (q *Queries) CountTransactionsByHeight(ctx context.Context, blockHeight int const createTransaction = `-- name: CreateTransaction :execresult INSERT INTO transactions ( - hash, block_height, block_index, type, raw_tx, signature + hash, block_height, block_index, type, subtype, raw_tx, signature, signer ) VALUES ( - ?, ?, ?, ?, ?, ? + ?, ?, ?, ?, ?, ?, ?, ? ) ` @@ -48,8 +48,10 @@ type CreateTransactionParams struct { BlockHeight int64 BlockIndex int64 Type string + Subtype string RawTx []byte Signature []byte + Signer []byte } func (q *Queries) CreateTransaction(ctx context.Context, arg CreateTransactionParams) (sql.Result, error) { @@ -58,13 +60,15 @@ func (q *Queries) CreateTransaction(ctx context.Context, arg CreateTransactionPa arg.BlockHeight, arg.BlockIndex, arg.Type, + arg.Subtype, arg.RawTx, arg.Signature, + arg.Signer, ) } const getTransaction = `-- name: GetTransaction :one -SELECT id, hash, block_height, block_index, type, raw_tx, signature FROM transactions +SELECT id, hash, block_height, block_index, type, raw_tx, signature, subtype, signer FROM transactions WHERE id = ? LIMIT 1 ` @@ -80,12 +84,14 @@ func (q *Queries) GetTransaction(ctx context.Context, id int64) (Transaction, er &i.Type, &i.RawTx, &i.Signature, + &i.Subtype, + &i.Signer, ) return i, err } const getTransactionByHash = `-- name: GetTransactionByHash :one -SELECT id, hash, block_height, block_index, type, raw_tx, signature FROM transactions +SELECT id, hash, block_height, block_index, type, raw_tx, signature, subtype, signer FROM transactions WHERE hash = ? LIMIT 1 ` @@ -101,12 +107,14 @@ func (q *Queries) GetTransactionByHash(ctx context.Context, hash types.Hash) (Tr &i.Type, &i.RawTx, &i.Signature, + &i.Subtype, + &i.Signer, ) return i, err } const getTransactionByHeightAndIndex = `-- name: GetTransactionByHeightAndIndex :one -SELECT id, hash, block_height, block_index, type, raw_tx, signature FROM transactions +SELECT id, hash, block_height, block_index, type, raw_tx, signature, subtype, signer FROM transactions WHERE block_height = ? AND block_index = ? LIMIT 1 ` @@ -127,20 +135,24 @@ func (q *Queries) GetTransactionByHeightAndIndex(ctx context.Context, arg GetTra &i.Type, &i.RawTx, &i.Signature, + &i.Subtype, + &i.Signer, ) return i, err } const searchTransactions = `-- name: SearchTransactions :many WITH results AS ( - SELECT id, hash, block_height, block_index, type, raw_tx, signature + SELECT id, hash, block_height, block_index, type, raw_tx, signature, subtype, signer FROM transactions WHERE ( (?3 = 0 OR block_height = ?3) AND (?4 = '' OR LOWER(type) = LOWER(?4)) + AND (?5 = '' OR LOWER(subtype) = LOWER(?5)) + AND (?6 = '' OR LOWER(HEX(signer)) = LOWER(?6)) ) ) -SELECT id, hash, block_height, block_index, type, raw_tx, signature, COUNT(*) OVER() AS total_count +SELECT id, hash, block_height, block_index, type, raw_tx, signature, subtype, signer, COUNT(*) OVER() AS total_count FROM results ORDER BY id DESC LIMIT ?2 @@ -152,6 +164,8 @@ type SearchTransactionsParams struct { Limit int64 BlockHeight interface{} TxType interface{} + TxSubtype interface{} + TxSigner interface{} } type SearchTransactionsRow struct { @@ -162,6 +176,8 @@ type SearchTransactionsRow struct { Type string RawTx []byte Signature []byte + Subtype string + Signer []byte TotalCount int64 } @@ -171,6 +187,8 @@ func (q *Queries) SearchTransactions(ctx context.Context, arg SearchTransactions arg.Limit, arg.BlockHeight, arg.TxType, + arg.TxSubtype, + arg.TxSigner, ) if err != nil { return nil, err @@ -187,6 +205,8 @@ func (q *Queries) SearchTransactions(ctx context.Context, arg SearchTransactions &i.Type, &i.RawTx, &i.Signature, + &i.Subtype, + &i.Signer, &i.TotalCount, ); err != nil { return nil, err diff --git a/vochain/indexer/indexertypes/types.go b/vochain/indexer/indexertypes/types.go index 1f6270596..d46257da6 100644 --- a/vochain/indexer/indexertypes/types.go +++ b/vochain/indexer/indexertypes/types.go @@ -183,6 +183,7 @@ type TransactionMetadata struct { BlockHeight uint32 `json:"blockHeight" format:"int32" example:"64924"` TxBlockIndex int32 `json:"transactionIndex" format:"int32" example:"0"` TxType string `json:"transactionType" enums:"vote,newProcess,admin,setProcess,registerKey,mintTokens,sendTokens,setTransactionCosts,setAccount,collectFaucet,setKeykeeper" example:"Vote"` + TxSubtype string `json:"transactionSubtype" enums:"newProcess,setProcessCensus,setProcessDuration" example:"setProcessCensus"` } func TransactionMetadataFromDB(dbtx *indexerdb.Transaction) *TransactionMetadata { @@ -192,6 +193,7 @@ func TransactionMetadataFromDB(dbtx *indexerdb.Transaction) *TransactionMetadata BlockHeight: uint32(dbtx.BlockHeight), TxBlockIndex: int32(dbtx.BlockIndex), TxType: dbtx.Type, + TxSubtype: dbtx.Subtype, } } @@ -202,6 +204,7 @@ func TransactionMetadataFromDBRow(dbtx *indexerdb.SearchTransactionsRow) *Transa BlockHeight: uint32(dbtx.BlockHeight), TxBlockIndex: int32(dbtx.BlockIndex), TxType: dbtx.Type, + TxSubtype: dbtx.Subtype, } } @@ -210,6 +213,7 @@ type Transaction struct { *TransactionMetadata RawTx types.HexBytes `json:"-"` Signature types.HexBytes `json:"-"` + Signer types.HexBytes `json:"-"` } // TransactionFromDB converts an indexerdb.Transaction into a Transaction @@ -218,6 +222,7 @@ func TransactionFromDB(dbtx *indexerdb.Transaction) *Transaction { TransactionMetadata: TransactionMetadataFromDB(dbtx), RawTx: dbtx.RawTx, Signature: dbtx.Signature, + Signer: dbtx.Signer, } } diff --git a/vochain/indexer/migrations/0015_alter_columns_table_transactions_2.sql b/vochain/indexer/migrations/0015_alter_columns_table_transactions_2.sql new file mode 100644 index 000000000..b114f56bd --- /dev/null +++ b/vochain/indexer/migrations/0015_alter_columns_table_transactions_2.sql @@ -0,0 +1,7 @@ +-- +goose Up +ALTER TABLE transactions ADD COLUMN subtype TEXT NOT NULL DEFAULT ''; +ALTER TABLE transactions ADD COLUMN signer BLOB NOT NULL DEFAULT x''; + +-- +goose Down +ALTER TABLE transactions DROP COLUMN signer; +ALTER TABLE transactions DROP COLUMN subtype; diff --git a/vochain/indexer/queries/transactions.sql b/vochain/indexer/queries/transactions.sql index 98307ea4c..4466a5387 100644 --- a/vochain/indexer/queries/transactions.sql +++ b/vochain/indexer/queries/transactions.sql @@ -1,8 +1,8 @@ -- name: CreateTransaction :execresult INSERT INTO transactions ( - hash, block_height, block_index, type, raw_tx, signature + hash, block_height, block_index, type, subtype, raw_tx, signature, signer ) VALUES ( - ?, ?, ?, ?, ?, ? + ?, ?, ?, ?, ?, ?, ?, ? ); -- name: GetTransaction :one @@ -34,6 +34,8 @@ WITH results AS ( WHERE ( (sqlc.arg(block_height) = 0 OR block_height = sqlc.arg(block_height)) AND (sqlc.arg(tx_type) = '' OR LOWER(type) = LOWER(sqlc.arg(tx_type))) + AND (sqlc.arg(tx_subtype) = '' OR LOWER(subtype) = LOWER(sqlc.arg(tx_subtype))) + AND (sqlc.arg(tx_signer) = '' OR LOWER(HEX(signer)) = LOWER(sqlc.arg(tx_signer))) ) ) SELECT *, COUNT(*) OVER() AS total_count diff --git a/vochain/indexer/transaction.go b/vochain/indexer/transaction.go index 4b8fe7b83..1cd73fdde 100644 --- a/vochain/indexer/transaction.go +++ b/vochain/indexer/transaction.go @@ -5,7 +5,9 @@ import ( "database/sql" "errors" "fmt" + "strings" + "go.vocdoni.io/dvote/crypto/ethereum" "go.vocdoni.io/dvote/log" "go.vocdoni.io/dvote/types" indexerdb "go.vocdoni.io/dvote/vochain/indexer/db" @@ -68,9 +70,9 @@ func (idx *Indexer) GetTransactionByHeightAndIndex(blockHeight, blockIndex int64 } // SearchTransactions returns the list of transactions indexed. -// height and txType are optional, if declared as zero-value will be ignored. +// blockHeight, txType, txSubtype and txSigner are optional, if declared as zero-value will be ignored. // The first one returned is the newest, so they are in descending order. -func (idx *Indexer) SearchTransactions(limit, offset int, blockHeight uint64, txType string) ([]*indexertypes.TransactionMetadata, uint64, error) { +func (idx *Indexer) SearchTransactions(limit, offset int, blockHeight uint64, txType, txSubtype, txSigner string) ([]*indexertypes.TransactionMetadata, uint64, error) { if offset < 0 { return nil, 0, fmt.Errorf("invalid value: offset cannot be %d", offset) } @@ -82,19 +84,15 @@ func (idx *Indexer) SearchTransactions(limit, offset int, blockHeight uint64, tx Offset: int64(offset), BlockHeight: blockHeight, TxType: txType, + TxSubtype: txSubtype, + TxSigner: txSigner, }) if err != nil { return nil, 0, err } list := []*indexertypes.TransactionMetadata{} for _, row := range results { - list = append(list, &indexertypes.TransactionMetadata{ - Index: uint64(row.ID), - Hash: row.Hash, - BlockHeight: uint32(row.BlockHeight), - TxBlockIndex: int32(row.BlockIndex), - TxType: row.Type, - }) + list = append(list, indexertypes.TransactionMetadataFromDBRow(&row)) } if len(results) == 0 { return list, 0, nil @@ -112,14 +110,26 @@ func (idx *Indexer) OnNewTx(tx *vochaintx.Tx, blockHeight uint32, txIndex int32) return } + signer := []byte{} + if len(tx.Signature) > 0 { // not all txs are signed, for example zk ones + addr, err := ethereum.AddrFromSignature(tx.SignedBody, tx.Signature) + if err != nil { + log.Errorw(err, "indexer cannot recover signer from signature") + return + } + signer = addr.Bytes() + } + queries := idx.blockTxQueries() if _, err := queries.CreateTransaction(context.TODO(), indexerdb.CreateTransactionParams{ Hash: tx.TxID[:], BlockHeight: int64(blockHeight), BlockIndex: int64(txIndex), Type: tx.TxModelType, + Subtype: strings.ToLower(tx.TxSubType()), RawTx: rawtx, - Signature: tx.Signature, + Signature: nonNullBytes(tx.Signature), + Signer: nonNullBytes(signer), }); err != nil { log.Errorw(err, "cannot index new transaction") } diff --git a/vochain/transaction/vochaintx/vochaintx.go b/vochain/transaction/vochaintx/vochaintx.go index f413e5e3b..e3e30c185 100644 --- a/vochain/transaction/vochaintx/vochaintx.go +++ b/vochain/transaction/vochaintx/vochaintx.go @@ -50,6 +50,32 @@ func (tx *Tx) Unmarshal(content []byte, chainID string) error { return nil } +// TxSubType returns the content of the "txtype" field inside the tx.Tx. +// +// The function determines the type of the transaction using Protocol Buffers reflection. +// If the field doesn't exist, it returns the empty string "". +func (tx *Tx) TxSubType() string { + txReflectDescriptor := tx.Tx.ProtoReflect().Descriptor().Oneofs().Get(0) + if txReflectDescriptor == nil { + return "" + } + whichOneTxModelType := tx.Tx.ProtoReflect().WhichOneof(txReflectDescriptor) + if whichOneTxModelType == nil { + return "" + } + // Get the value of the selected field in the oneof + fieldValue := tx.Tx.ProtoReflect().Get(whichOneTxModelType) + // Now, fieldValue is a protoreflect.Value, retrieve the txtype field + txtypeFieldDescriptor := fieldValue.Message().Descriptor().Fields().ByName("txtype") + if txtypeFieldDescriptor == nil { + return "" + } + // Get the integer value of txtype as protoreflect.EnumNumber + enumNumber := fieldValue.Message().Get(txtypeFieldDescriptor).Enum() + // Convert the EnumNumber to a string using the EnumType descriptor + return string(txtypeFieldDescriptor.Enum().Values().ByNumber(enumNumber).Name()) +} + // TxKey computes the checksum of the tx func TxKey(tx []byte) [32]byte { return comettypes.Tx(tx).Key()