From 0e278ca22b512ecf8470386fdf8a6021a400fc6d Mon Sep 17 00:00:00 2001 From: Ori Newman Date: Thu, 31 Oct 2019 11:59:56 +0200 Subject: [PATCH] [NOD-350] Implement testnet faucet (#438) * [NOD-350] Implement testnet faucet * [NOD-350] Add JSON annotations to api server response types * [NOD-350] Fix IP check query, update IP usage with upsert, and make IP a primary key * [NOD-377] Remove redundant float conversion * [NOD-377] Change not current database error message * [NOD-377] change API route from /money_request to /request_money * [NOD-377] Add a constant for 24 hours * [NOD-377] Remove redundant call for getWalletUTXOSet() * [NOD-377] Condition refactoring * [NOD-377] Fix POST request to API server content type * [NOD-350] Rename day -> timeBetweenRequests * [NOD-377] Rename timeBetweenRequests -> minRequestInterval, timeBefore24Hours -> minRequestInterval * [NOD-350] Rename file responsetypes -> response_types * [NOD-350] Rename convertTxModelToTxResponse -> convertTxDBModelToTxResponse * [NOD-350] Explicitly select blue_score in fetchSelectedTipBlueScore * [NOD-350] Refactor and add comments * [NOD-350] Make calcFee use MassPerTxByte * [NOD-350] Convert IP column to varchar(39) to allow ipv6 addresses * [NOD-350] Add comments to isFundedAndIsChangeOutputRequired * [NOD-350] Remove approximateConfirmationsForCoinbaseMaturity * [NOD-350] Fix comments --- .../request_types.go | 2 +- apiserver/apimodels/response_types.go | 63 ++++ apiserver/controllers/block.go | 35 +- apiserver/controllers/common.go | 63 ++++ apiserver/controllers/feeestimate.go | 9 +- apiserver/controllers/response_types.go | 113 ------ apiserver/controllers/transaction.go | 139 +++++--- apiserver/{models => dbmodels}/models.go | 2 +- apiserver/server/routes.go | 135 ++----- apiserver/server/server.go | 9 +- apiserver/sync.go | 259 +++++++------- blockdag/utxoset.go | 2 +- blockdag/validate.go | 25 +- cmd/txgen/config.go | 2 +- cmd/txgen/txloop.go | 36 +- config/config.go | 2 +- faucet/config/config.go | 112 ++++++ faucet/database/database.go | 150 ++++++++ faucet/database/log.go | 9 + faucet/faucet.go | 331 ++++++++++++++++++ faucet/ip_usage.go | 65 ++++ faucet/log.go | 11 + faucet/main.go | 89 +++++ .../000001_create_ip_uses_table.down.sql | 1 + .../000001_create_ip_uses_table.up.sql | 6 + faucet/server.go | 81 +++++ go.mod | 2 + go.sum | 8 + .../utils => httpserverutils}/context.go | 34 +- {apiserver/utils => httpserverutils}/error.go | 39 ++- {apiserver/utils => httpserverutils}/log.go | 2 +- .../server => httpserverutils}/middlewares.go | 28 +- httpserverutils/request.go | 58 +++ server/rpc/handle_get_blocks.go | 2 +- util/panics/panics.go | 2 +- 35 files changed, 1450 insertions(+), 476 deletions(-) rename apiserver/{controllers => apimodels}/request_types.go (88%) create mode 100644 apiserver/apimodels/response_types.go create mode 100644 apiserver/controllers/common.go delete mode 100644 apiserver/controllers/response_types.go rename apiserver/{models => dbmodels}/models.go (99%) create mode 100644 faucet/config/config.go create mode 100644 faucet/database/database.go create mode 100644 faucet/database/log.go create mode 100644 faucet/faucet.go create mode 100644 faucet/ip_usage.go create mode 100644 faucet/log.go create mode 100644 faucet/main.go create mode 100644 faucet/migrations/000001_create_ip_uses_table.down.sql create mode 100644 faucet/migrations/000001_create_ip_uses_table.up.sql create mode 100644 faucet/server.go rename {apiserver/utils => httpserverutils}/context.go (56%) rename {apiserver/utils => httpserverutils}/error.go (71%) rename {apiserver/utils => httpserverutils}/log.go (89%) rename {apiserver/server => httpserverutils}/middlewares.go (50%) create mode 100644 httpserverutils/request.go diff --git a/apiserver/controllers/request_types.go b/apiserver/apimodels/request_types.go similarity index 88% rename from apiserver/controllers/request_types.go rename to apiserver/apimodels/request_types.go index 83a9be214..6eeb1e66f 100644 --- a/apiserver/controllers/request_types.go +++ b/apiserver/apimodels/request_types.go @@ -1,4 +1,4 @@ -package controllers +package apimodels // RawTransaction represents a raw transaction posted to the API server type RawTransaction struct { diff --git a/apiserver/apimodels/response_types.go b/apiserver/apimodels/response_types.go new file mode 100644 index 000000000..5a9489ba8 --- /dev/null +++ b/apiserver/apimodels/response_types.go @@ -0,0 +1,63 @@ +package apimodels + +// TransactionResponse is a json representation of a transaction +type TransactionResponse struct { + TransactionHash string `json:"transactionHash"` + TransactionID string `json:"transactionId"` + AcceptingBlockHash string `json:"acceptingBlockHash,omitempty"` + AcceptingBlockBlueScore uint64 `json:"acceptingBlockBlueScore,omitempty"` + SubnetworkID string `json:"subnetworkId"` + LockTime uint64 `json:"lockTime"` + Gas uint64 `json:"gas,omitempty"` + PayloadHash string `json:"payloadHash,omitempty"` + Payload string `json:"payload,omitempty"` + Inputs []*TransactionInputResponse `json:"inputs"` + Outputs []*TransactionOutputResponse `json:"outputs"` + Mass uint64 `json:"mass"` +} + +// TransactionOutputResponse is a json representation of a transaction output +type TransactionOutputResponse struct { + TransactionID string `json:"transactionId,omitempty"` + Value uint64 `json:"value"` + ScriptPubKey string `json:"scriptPubKey"` + Address string `json:"address,omitempty"` + AcceptingBlockHash *string `json:"acceptingBlockHash,omitempty"` + AcceptingBlockBlueScore uint64 `json:"acceptingBlockBlueScore,omitempty"` + Index uint32 `json:"index"` + IsCoinbase *bool `json:"isCoinbase,omitempty"` + Confirmations *uint64 `json:"confirmations,omitempty"` +} + +// TransactionInputResponse is a json representation of a transaction input +type TransactionInputResponse struct { + TransactionID string `json:"transactionId,omitempty"` + PreviousTransactionID string `json:"previousTransactionId"` + PreviousTransactionOutputIndex uint32 `json:"previousTransactionOutputIndex"` + SignatureScript string `json:"signatureScript"` + Sequence uint64 `json:"sequence"` + Address string `json:"address"` +} + +// BlockResponse is a json representation of a block +type BlockResponse struct { + BlockHash string `json:"blockHash"` + Version int32 `json:"version"` + HashMerkleRoot string `json:"hashMerkleRoot"` + AcceptedIDMerkleRoot string `json:"acceptedIDMerkleRoot"` + UTXOCommitment string `json:"utxoCommitment"` + Timestamp uint64 `json:"timestamp"` + Bits uint32 `json:"bits"` + Nonce uint64 `json:"nonce"` + AcceptingBlockHash *string `json:"acceptingBlockHash"` + BlueScore uint64 `json:"blueScore"` + IsChainBlock bool `json:"isChainBlock"` + Mass uint64 `json:"mass"` +} + +// FeeEstimateResponse is a json representation of a fee estimate +type FeeEstimateResponse struct { + HighPriority float64 `json:"highPriority"` + NormalPriority float64 `json:"normalPriority"` + LowPriority float64 `json:"lowPriority"` +} diff --git a/apiserver/controllers/block.go b/apiserver/controllers/block.go index fb23a56d2..6f3157a42 100644 --- a/apiserver/controllers/block.go +++ b/apiserver/controllers/block.go @@ -3,11 +3,12 @@ package controllers import ( "encoding/hex" "fmt" + "github.com/daglabs/btcd/apiserver/apimodels" + "github.com/daglabs/btcd/apiserver/dbmodels" + "github.com/daglabs/btcd/httpserverutils" "net/http" "github.com/daglabs/btcd/apiserver/database" - "github.com/daglabs/btcd/apiserver/models" - "github.com/daglabs/btcd/apiserver/utils" "github.com/daglabs/btcd/util/daghash" ) @@ -26,38 +27,38 @@ const ( const maxGetBlocksLimit = 100 // GetBlockByHashHandler returns a block by a given hash. -func GetBlockByHashHandler(blockHash string) (interface{}, *utils.HandlerError) { +func GetBlockByHashHandler(blockHash string) (interface{}, *httpserverutils.HandlerError) { if bytes, err := hex.DecodeString(blockHash); err != nil || len(bytes) != daghash.HashSize { - return nil, utils.NewHandlerError(http.StatusUnprocessableEntity, + return nil, httpserverutils.NewHandlerError(http.StatusUnprocessableEntity, fmt.Sprintf("The given block hash is not a hex-encoded %d-byte hash.", daghash.HashSize)) } db, err := database.DB() if err != nil { - return nil, utils.NewInternalServerHandlerError(err.Error()) + return nil, httpserverutils.NewInternalServerHandlerError(err.Error()) } - block := &models.Block{} - dbResult := db.Where(&models.Block{BlockHash: blockHash}).Preload("AcceptingBlock").First(block) + block := &dbmodels.Block{} + dbResult := db.Where(&dbmodels.Block{BlockHash: blockHash}).Preload("AcceptingBlock").First(block) dbErrors := dbResult.GetErrors() - if utils.IsDBRecordNotFoundError(dbErrors) { - return nil, utils.NewHandlerError(http.StatusNotFound, "No block with the given block hash was found.") + if httpserverutils.IsDBRecordNotFoundError(dbErrors) { + return nil, httpserverutils.NewHandlerError(http.StatusNotFound, "No block with the given block hash was found.") } - if utils.HasDBError(dbErrors) { - return nil, utils.NewHandlerErrorFromDBErrors("Some errors where encountered when loading transactions from the database:", dbResult.GetErrors()) + if httpserverutils.HasDBError(dbErrors) { + return nil, httpserverutils.NewHandlerErrorFromDBErrors("Some errors were encountered when loading transactions from the database:", dbResult.GetErrors()) } return convertBlockModelToBlockResponse(block), nil } // GetBlocksHandler searches for all blocks -func GetBlocksHandler(order string, skip uint64, limit uint64) (interface{}, *utils.HandlerError) { +func GetBlocksHandler(order string, skip uint64, limit uint64) (interface{}, *httpserverutils.HandlerError) { if limit > maxGetBlocksLimit { - return nil, utils.NewHandlerError(http.StatusUnprocessableEntity, fmt.Sprintf("The maximum allowed value for the limit is %d", maxGetTransactionsLimit)) + return nil, httpserverutils.NewHandlerError(http.StatusUnprocessableEntity, fmt.Sprintf("The maximum allowed value for the limit is %d", maxGetTransactionsLimit)) } - blocks := []*models.Block{} + blocks := []*dbmodels.Block{} db, err := database.DB() if err != nil { - return nil, utils.NewHandlerError(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) + return nil, httpserverutils.NewHandlerError(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) } query := db. Limit(limit). @@ -68,10 +69,10 @@ func GetBlocksHandler(order string, skip uint64, limit uint64) (interface{}, *ut } else if order == OrderDescending { query = query.Order("`id` DESC") } else { - return nil, utils.NewHandlerError(http.StatusUnprocessableEntity, fmt.Sprintf("'%s' is not a valid order", order)) + return nil, httpserverutils.NewHandlerError(http.StatusUnprocessableEntity, fmt.Sprintf("'%s' is not a valid order", order)) } query.Find(&blocks) - blockResponses := make([]*blockResponse, len(blocks)) + blockResponses := make([]*apimodels.BlockResponse, len(blocks)) for i, block := range blocks { blockResponses[i] = convertBlockModelToBlockResponse(block) } diff --git a/apiserver/controllers/common.go b/apiserver/controllers/common.go new file mode 100644 index 000000000..a379b4828 --- /dev/null +++ b/apiserver/controllers/common.go @@ -0,0 +1,63 @@ +package controllers + +import ( + "encoding/hex" + "github.com/daglabs/btcd/apiserver/apimodels" + "github.com/daglabs/btcd/apiserver/dbmodels" + "github.com/daglabs/btcd/btcjson" +) + +func convertTxDBModelToTxResponse(tx *dbmodels.Transaction) *apimodels.TransactionResponse { + txRes := &apimodels.TransactionResponse{ + TransactionHash: tx.TransactionHash, + TransactionID: tx.TransactionID, + AcceptingBlockHash: tx.AcceptingBlock.BlockHash, + AcceptingBlockBlueScore: tx.AcceptingBlock.BlueScore, + SubnetworkID: tx.Subnetwork.SubnetworkID, + LockTime: tx.LockTime, + Gas: tx.Gas, + PayloadHash: tx.PayloadHash, + Payload: hex.EncodeToString(tx.Payload), + Inputs: make([]*apimodels.TransactionInputResponse, len(tx.TransactionInputs)), + Outputs: make([]*apimodels.TransactionOutputResponse, len(tx.TransactionOutputs)), + Mass: tx.Mass, + } + for i, txOut := range tx.TransactionOutputs { + txRes.Outputs[i] = &apimodels.TransactionOutputResponse{ + Value: txOut.Value, + ScriptPubKey: hex.EncodeToString(txOut.ScriptPubKey), + Address: txOut.Address.Address, + Index: txOut.Index, + } + } + for i, txIn := range tx.TransactionInputs { + txRes.Inputs[i] = &apimodels.TransactionInputResponse{ + PreviousTransactionID: txIn.PreviousTransactionOutput.Transaction.TransactionID, + PreviousTransactionOutputIndex: txIn.PreviousTransactionOutput.Index, + SignatureScript: hex.EncodeToString(txIn.SignatureScript), + Sequence: txIn.Sequence, + Address: txIn.PreviousTransactionOutput.Address.Address, + } + } + return txRes +} + +func convertBlockModelToBlockResponse(block *dbmodels.Block) *apimodels.BlockResponse { + blockRes := &apimodels.BlockResponse{ + BlockHash: block.BlockHash, + Version: block.Version, + HashMerkleRoot: block.HashMerkleRoot, + AcceptedIDMerkleRoot: block.AcceptedIDMerkleRoot, + UTXOCommitment: block.UTXOCommitment, + Timestamp: uint64(block.Timestamp.Unix()), + Bits: block.Bits, + Nonce: block.Nonce, + BlueScore: block.BlueScore, + IsChainBlock: block.IsChainBlock, + Mass: block.Mass, + } + if block.AcceptingBlock != nil { + blockRes.AcceptingBlockHash = btcjson.String(block.AcceptingBlock.BlockHash) + } + return blockRes +} diff --git a/apiserver/controllers/feeestimate.go b/apiserver/controllers/feeestimate.go index b23cbd099..0984fb389 100644 --- a/apiserver/controllers/feeestimate.go +++ b/apiserver/controllers/feeestimate.go @@ -1,11 +1,14 @@ package controllers -import "github.com/daglabs/btcd/apiserver/utils" +import ( + "github.com/daglabs/btcd/apiserver/apimodels" + "github.com/daglabs/btcd/httpserverutils" +) // GetFeeEstimatesHandler returns the fee estimates for different priorities // for accepting a transaction in the DAG. -func GetFeeEstimatesHandler() (interface{}, *utils.HandlerError) { - return &feeEstimateResponse{ +func GetFeeEstimatesHandler() (interface{}, *httpserverutils.HandlerError) { + return &apimodels.FeeEstimateResponse{ HighPriority: 3, NormalPriority: 2, LowPriority: 1, diff --git a/apiserver/controllers/response_types.go b/apiserver/controllers/response_types.go deleted file mode 100644 index 96ab5413b..000000000 --- a/apiserver/controllers/response_types.go +++ /dev/null @@ -1,113 +0,0 @@ -package controllers - -import ( - "encoding/hex" - "github.com/daglabs/btcd/apiserver/models" - "github.com/daglabs/btcd/btcjson" -) - -type transactionResponse struct { - TransactionHash string `json:"transactionHash"` - TransactionID string `json:"transactionId"` - AcceptingBlockHash string `json:"acceptingBlockHash,omitempty"` - AcceptingBlockBlueScore uint64 `json:"acceptingBlockBlueScore,omitempty"` - SubnetworkID string `json:"subnetworkId"` - LockTime uint64 `json:"lockTime"` - Gas uint64 `json:"gas,omitempty"` - PayloadHash string `json:"payloadHash,omitempty"` - Payload string `json:"payload,omitempty"` - Inputs []*transactionInputResponse `json:"inputs"` - Outputs []*transactionOutputResponse `json:"outputs"` - Mass uint64 `json:"mass"` -} - -type transactionOutputResponse struct { - TransactionID string `json:"transactionId,omitempty"` - Value uint64 `json:"value"` - ScriptPubKey string `json:"scriptPubKey"` - Address string `json:"address,omitempty"` - AcceptingBlockHash string `json:"acceptingBlockHash,omitempty"` - AcceptingBlockBlueScore uint64 `json:"acceptingBlockBlueScore,omitempty"` -} - -type transactionInputResponse struct { - TransactionID string `json:"transactionId,omitempty"` - PreviousTransactionID string `json:"previousTransactionId"` - PreviousTransactionOutputIndex uint32 `json:"previousTransactionOutputIndex"` - SignatureScript string `json:"signatureScript"` - Sequence uint64 `json:"sequence"` - Address string `json:"address"` -} - -type blockResponse struct { - BlockHash string - Version int32 - HashMerkleRoot string - AcceptedIDMerkleRoot string - UTXOCommitment string - Timestamp uint64 - Bits uint32 - Nonce uint64 - AcceptingBlockHash *string - BlueScore uint64 - IsChainBlock bool - Mass uint64 -} - -type feeEstimateResponse struct { - HighPriority, NormalPriority, LowPriority float64 -} - -func convertTxModelToTxResponse(tx *models.Transaction) *transactionResponse { - txRes := &transactionResponse{ - TransactionHash: tx.TransactionHash, - TransactionID: tx.TransactionID, - AcceptingBlockHash: tx.AcceptingBlock.BlockHash, - AcceptingBlockBlueScore: tx.AcceptingBlock.BlueScore, - SubnetworkID: tx.Subnetwork.SubnetworkID, - LockTime: tx.LockTime, - Gas: tx.Gas, - PayloadHash: tx.PayloadHash, - Payload: hex.EncodeToString(tx.Payload), - Inputs: make([]*transactionInputResponse, len(tx.TransactionInputs)), - Outputs: make([]*transactionOutputResponse, len(tx.TransactionOutputs)), - Mass: tx.Mass, - } - for i, txOut := range tx.TransactionOutputs { - txRes.Outputs[i] = &transactionOutputResponse{ - Value: txOut.Value, - ScriptPubKey: hex.EncodeToString(txOut.ScriptPubKey), - Address: txOut.Address.Address, - } - } - for i, txIn := range tx.TransactionInputs { - txRes.Inputs[i] = &transactionInputResponse{ - PreviousTransactionID: txIn.PreviousTransactionOutput.Transaction.TransactionID, - PreviousTransactionOutputIndex: txIn.PreviousTransactionOutput.Index, - SignatureScript: hex.EncodeToString(txIn.SignatureScript), - Sequence: txIn.Sequence, - Address: txIn.PreviousTransactionOutput.Address.Address, - } - } - return txRes -} - -func convertBlockModelToBlockResponse(block *models.Block) *blockResponse { - blockRes := &blockResponse{ - BlockHash: block.BlockHash, - Version: block.Version, - HashMerkleRoot: block.HashMerkleRoot, - AcceptedIDMerkleRoot: block.AcceptedIDMerkleRoot, - UTXOCommitment: block.UTXOCommitment, - Timestamp: uint64(block.Timestamp.Unix()), - Bits: block.Bits, - Nonce: block.Nonce, - BlueScore: block.BlueScore, - IsChainBlock: block.IsChainBlock, - Mass: block.Mass, - } - if block.AcceptingBlock != nil { - blockRes.AcceptingBlockHash = btcjson.String(block.AcceptingBlock.BlockHash) - } - return blockRes -} diff --git a/apiserver/controllers/transaction.go b/apiserver/controllers/transaction.go index bc6bba59c..e3bdfa7f5 100644 --- a/apiserver/controllers/transaction.go +++ b/apiserver/controllers/transaction.go @@ -5,12 +5,15 @@ import ( "encoding/hex" "encoding/json" "fmt" + "github.com/daglabs/btcd/apiserver/apimodels" + "github.com/daglabs/btcd/apiserver/dbmodels" + "github.com/daglabs/btcd/blockdag" + "github.com/daglabs/btcd/httpserverutils" + "github.com/daglabs/btcd/util/subnetworkid" "net/http" "github.com/daglabs/btcd/apiserver/database" "github.com/daglabs/btcd/apiserver/jsonrpc" - "github.com/daglabs/btcd/apiserver/models" - "github.com/daglabs/btcd/apiserver/utils" "github.com/daglabs/btcd/btcjson" "github.com/daglabs/btcd/util/daghash" "github.com/daglabs/btcd/wire" @@ -20,69 +23,69 @@ import ( const maxGetTransactionsLimit = 1000 // GetTransactionByIDHandler returns a transaction by a given transaction ID. -func GetTransactionByIDHandler(txID string) (interface{}, *utils.HandlerError) { +func GetTransactionByIDHandler(txID string) (interface{}, *httpserverutils.HandlerError) { if bytes, err := hex.DecodeString(txID); err != nil || len(bytes) != daghash.TxIDSize { - return nil, utils.NewHandlerError(http.StatusUnprocessableEntity, + return nil, httpserverutils.NewHandlerError(http.StatusUnprocessableEntity, fmt.Sprintf("The given txid is not a hex-encoded %d-byte hash.", daghash.TxIDSize)) } db, err := database.DB() if err != nil { - return nil, utils.NewInternalServerHandlerError(err.Error()) + return nil, httpserverutils.NewInternalServerHandlerError(err.Error()) } - tx := &models.Transaction{} - query := db.Where(&models.Transaction{TransactionID: txID}) + tx := &dbmodels.Transaction{} + query := db.Where(&dbmodels.Transaction{TransactionID: txID}) dbResult := addTxPreloadedFields(query).First(&tx) dbErrors := dbResult.GetErrors() - if utils.IsDBRecordNotFoundError(dbErrors) { - return nil, utils.NewHandlerError(http.StatusNotFound, "No transaction with the given txid was found.") + if httpserverutils.IsDBRecordNotFoundError(dbErrors) { + return nil, httpserverutils.NewHandlerError(http.StatusNotFound, "No transaction with the given txid was found.") } - if utils.HasDBError(dbErrors) { - return nil, utils.NewHandlerErrorFromDBErrors("Some errors where encountered when loading transaction from the database:", dbErrors) + if httpserverutils.HasDBError(dbErrors) { + return nil, httpserverutils.NewHandlerErrorFromDBErrors("Some errors were encountered when loading transaction from the database:", dbErrors) } - return convertTxModelToTxResponse(tx), nil + return convertTxDBModelToTxResponse(tx), nil } // GetTransactionByHashHandler returns a transaction by a given transaction hash. -func GetTransactionByHashHandler(txHash string) (interface{}, *utils.HandlerError) { +func GetTransactionByHashHandler(txHash string) (interface{}, *httpserverutils.HandlerError) { if bytes, err := hex.DecodeString(txHash); err != nil || len(bytes) != daghash.HashSize { - return nil, utils.NewHandlerError(http.StatusUnprocessableEntity, + return nil, httpserverutils.NewHandlerError(http.StatusUnprocessableEntity, fmt.Sprintf("The given txhash is not a hex-encoded %d-byte hash.", daghash.HashSize)) } db, err := database.DB() if err != nil { - return nil, utils.NewHandlerError(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) + return nil, httpserverutils.NewHandlerError(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) } - tx := &models.Transaction{} - query := db.Where(&models.Transaction{TransactionHash: txHash}) + tx := &dbmodels.Transaction{} + query := db.Where(&dbmodels.Transaction{TransactionHash: txHash}) dbResult := addTxPreloadedFields(query).First(&tx) dbErrors := dbResult.GetErrors() - if utils.IsDBRecordNotFoundError(dbErrors) { - return nil, utils.NewHandlerError(http.StatusNotFound, "No transaction with the given txhash was found.") + if httpserverutils.IsDBRecordNotFoundError(dbErrors) { + return nil, httpserverutils.NewHandlerError(http.StatusNotFound, "No transaction with the given txhash was found.") } - if utils.HasDBError(dbErrors) { - return nil, utils.NewHandlerErrorFromDBErrors("Some errors where encountered when loading transaction from the database:", dbErrors) + if httpserverutils.HasDBError(dbErrors) { + return nil, httpserverutils.NewHandlerErrorFromDBErrors("Some errors were encountered when loading transaction from the database:", dbErrors) } - return convertTxModelToTxResponse(tx), nil + return convertTxDBModelToTxResponse(tx), nil } // GetTransactionsByAddressHandler searches for all transactions // where the given address is either an input or an output. -func GetTransactionsByAddressHandler(address string, skip uint64, limit uint64) (interface{}, *utils.HandlerError) { +func GetTransactionsByAddressHandler(address string, skip uint64, limit uint64) (interface{}, *httpserverutils.HandlerError) { if limit > maxGetTransactionsLimit { - return nil, utils.NewHandlerError(http.StatusUnprocessableEntity, + return nil, httpserverutils.NewHandlerError(http.StatusUnprocessableEntity, fmt.Sprintf("The maximum allowed value for the limit is %d", maxGetTransactionsLimit)) } db, err := database.DB() if err != nil { - return nil, utils.NewHandlerError(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) + return nil, httpserverutils.NewHandlerError(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) } - txs := []*models.Transaction{} + txs := []*dbmodels.Transaction{} query := db. Joins("LEFT JOIN `transaction_outputs` ON `transaction_outputs`.`transaction_id` = `transactions`.`id`"). Joins("LEFT JOIN `addresses` AS `out_addresses` ON `out_addresses`.`id` = `transaction_outputs`.`address_id`"). @@ -96,40 +99,80 @@ func GetTransactionsByAddressHandler(address string, skip uint64, limit uint64) Order("`transactions`.`id` ASC") dbResult := addTxPreloadedFields(query).Find(&txs) dbErrors := dbResult.GetErrors() - if utils.HasDBError(dbErrors) { - return nil, utils.NewHandlerErrorFromDBErrors("Some errors where encountered when loading transactions from the database:", dbErrors) + if httpserverutils.HasDBError(dbErrors) { + return nil, httpserverutils.NewHandlerErrorFromDBErrors("Some errors were encountered when loading transactions from the database:", dbErrors) } - txResponses := make([]*transactionResponse, len(txs)) + txResponses := make([]*apimodels.TransactionResponse, len(txs)) for i, tx := range txs { - txResponses[i] = convertTxModelToTxResponse(tx) + txResponses[i] = convertTxDBModelToTxResponse(tx) } return txResponses, nil } -// GetUTXOsByAddressHandler searches for all UTXOs that belong to a certain address. -func GetUTXOsByAddressHandler(address string) (interface{}, *utils.HandlerError) { +func fetchSelectedTipBlueScore() (uint64, *httpserverutils.HandlerError) { db, err := database.DB() if err != nil { - return nil, utils.NewHandlerError(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) + return 0, httpserverutils.NewInternalServerHandlerError(err.Error()) + } + block := &dbmodels.Block{} + dbResult := db.Order("blue_score DESC"). + Where(&dbmodels.Block{IsChainBlock: true}). + Select("blue_score"). + First(block) + dbErrors := dbResult.GetErrors() + if httpserverutils.HasDBError(dbErrors) { + return 0, httpserverutils.NewHandlerErrorFromDBErrors("Some errors were encountered when loading transactions from the database:", dbErrors) + } + return block.BlueScore, nil +} + +// GetUTXOsByAddressHandler searches for all UTXOs that belong to a certain address. +func GetUTXOsByAddressHandler(address string) (interface{}, *httpserverutils.HandlerError) { + db, err := database.DB() + if err != nil { + return nil, httpserverutils.NewInternalServerHandlerError(err.Error()) } - var transactionOutputs []*models.TransactionOutput + var transactionOutputs []*dbmodels.TransactionOutput dbErrors := db. Joins("LEFT JOIN `addresses` ON `addresses`.`id` = `transaction_outputs`.`address_id`"). Where("`addresses`.`address` = ? AND `transaction_outputs`.`is_spent` = 0", address). Preload("Transaction.AcceptingBlock"). + Preload("Transaction.Subnetwork"). Find(&transactionOutputs).GetErrors() if len(dbErrors) > 0 { - return nil, utils.NewHandlerErrorFromDBErrors("Some errors where encountered when loading UTXOs from the database:", dbErrors) + return nil, httpserverutils.NewHandlerErrorFromDBErrors("Some errors were encountered when loading UTXOs from the database:", dbErrors) } - UTXOsResponses := make([]*transactionOutputResponse, len(transactionOutputs)) + selectedTipBlueScore, hErr := fetchSelectedTipBlueScore() + if hErr != nil { + return nil, hErr + } + + UTXOsResponses := make([]*apimodels.TransactionOutputResponse, len(transactionOutputs)) for i, transactionOutput := range transactionOutputs { - UTXOsResponses[i] = &transactionOutputResponse{ + subnetworkID := &subnetworkid.SubnetworkID{} + err := subnetworkid.Decode(subnetworkID, transactionOutput.Transaction.Subnetwork.SubnetworkID) + if err != nil { + return nil, httpserverutils.NewInternalServerHandlerError(fmt.Sprintf("Couldn't decode subnetwork id %s: %s", transactionOutput.Transaction.Subnetwork.SubnetworkID, err)) + } + var acceptingBlockHash *string + var confirmations uint64 + acceptingBlockBlueScore := blockdag.UnacceptedBlueScore + if transactionOutput.Transaction.AcceptingBlock != nil { + acceptingBlockHash = btcjson.String(transactionOutput.Transaction.AcceptingBlock.BlockHash) + acceptingBlockBlueScore = transactionOutput.Transaction.AcceptingBlock.BlueScore + confirmations = selectedTipBlueScore - acceptingBlockBlueScore + } + UTXOsResponses[i] = &apimodels.TransactionOutputResponse{ + TransactionID: transactionOutput.Transaction.TransactionID, Value: transactionOutput.Value, ScriptPubKey: hex.EncodeToString(transactionOutput.ScriptPubKey), - AcceptingBlockHash: transactionOutput.Transaction.AcceptingBlock.BlockHash, - AcceptingBlockBlueScore: transactionOutput.Transaction.AcceptingBlock.BlueScore, + AcceptingBlockHash: acceptingBlockHash, + AcceptingBlockBlueScore: acceptingBlockBlueScore, + Index: transactionOutput.Index, + IsCoinbase: btcjson.Bool(subnetworkID.IsEqual(subnetworkid.SubnetworkIDCoinbase)), + Confirmations: btcjson.Uint64(confirmations), } } return UTXOsResponses, nil @@ -145,23 +188,23 @@ func addTxPreloadedFields(query *gorm.DB) *gorm.DB { } // PostTransaction forwards a raw transaction to the JSON-RPC API server -func PostTransaction(requestBody []byte) *utils.HandlerError { +func PostTransaction(requestBody []byte) *httpserverutils.HandlerError { client, err := jsonrpc.GetClient() if err != nil { - return utils.NewInternalServerHandlerError(err.Error()) + return httpserverutils.NewInternalServerHandlerError(err.Error()) } - rawTx := &RawTransaction{} + rawTx := &apimodels.RawTransaction{} err = json.Unmarshal(requestBody, rawTx) if err != nil { - return utils.NewHandlerErrorWithCustomClientMessage(http.StatusUnprocessableEntity, + return httpserverutils.NewHandlerErrorWithCustomClientMessage(http.StatusUnprocessableEntity, fmt.Sprintf("Error unmarshalling request body: %s", err), "The request body is not json-formatted") } txBytes, err := hex.DecodeString(rawTx.RawTransaction) if err != nil { - return utils.NewHandlerErrorWithCustomClientMessage(http.StatusUnprocessableEntity, + return httpserverutils.NewHandlerErrorWithCustomClientMessage(http.StatusUnprocessableEntity, fmt.Sprintf("Error decoding hex raw transaction: %s", err), "The raw transaction is not a hex-encoded transaction") } @@ -170,17 +213,17 @@ func PostTransaction(requestBody []byte) *utils.HandlerError { tx := &wire.MsgTx{} err = tx.BtcDecode(txReader, 0) if err != nil { - return utils.NewHandlerErrorWithCustomClientMessage(http.StatusUnprocessableEntity, + return httpserverutils.NewHandlerErrorWithCustomClientMessage(http.StatusUnprocessableEntity, fmt.Sprintf("Error decoding raw transaction: %s", err), "Error decoding raw transaction") } _, err = client.SendRawTransaction(tx, true) if err != nil { - if rpcErr, ok := err.(btcjson.RPCError); ok && rpcErr.Code == btcjson.ErrRPCVerify { - return utils.NewHandlerError(http.StatusInternalServerError, rpcErr.Message) + if rpcErr, ok := err.(*btcjson.RPCError); ok && rpcErr.Code == btcjson.ErrRPCVerify { + return httpserverutils.NewHandlerError(http.StatusInternalServerError, rpcErr.Message) } - return utils.NewHandlerError(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) + return httpserverutils.NewHandlerError(http.StatusInternalServerError, http.StatusText(http.StatusInternalServerError)) } return nil diff --git a/apiserver/models/models.go b/apiserver/dbmodels/models.go similarity index 99% rename from apiserver/models/models.go rename to apiserver/dbmodels/models.go index 666748e84..3a3568d48 100644 --- a/apiserver/models/models.go +++ b/apiserver/dbmodels/models.go @@ -1,4 +1,4 @@ -package models +package dbmodels import ( "time" diff --git a/apiserver/server/routes.go b/apiserver/server/routes.go index 1040bdff3..cb60d3557 100644 --- a/apiserver/server/routes.go +++ b/apiserver/server/routes.go @@ -1,14 +1,12 @@ package server import ( - "encoding/json" "fmt" - "io/ioutil" + "github.com/daglabs/btcd/httpserverutils" "net/http" "strconv" "github.com/daglabs/btcd/apiserver/controllers" - "github.com/daglabs/btcd/apiserver/utils" "github.com/gorilla/mux" ) @@ -31,78 +29,7 @@ const ( defaultGetBlocksOrder = controllers.OrderAscending ) -type handlerFunc func(ctx *utils.APIServerContext, routeParams map[string]string, queryParams map[string]string, requestBody []byte) ( - interface{}, *utils.HandlerError) - -func makeHandler(handler handlerFunc) func(http.ResponseWriter, *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - ctx := utils.ToAPIServerContext(r.Context()) - - var requestBody []byte - if r.Method == "POST" { - var err error - requestBody, err = ioutil.ReadAll(r.Body) - if err != nil { - sendErr(ctx, w, utils.NewHandlerError(500, "Internal server error occured")) - } - } - - flattenedQueryParams, hErr := flattenQueryParams(r.URL.Query()) - if hErr != nil { - sendErr(ctx, w, hErr) - return - } - - response, hErr := handler(ctx, mux.Vars(r), flattenedQueryParams, requestBody) - if hErr != nil { - sendErr(ctx, w, hErr) - return - } - if response != nil { - sendJSONResponse(w, response) - } - } -} - -func flattenQueryParams(queryParams map[string][]string) (map[string]string, *utils.HandlerError) { - flattenedMap := make(map[string]string) - for param, valuesSlice := range queryParams { - if len(valuesSlice) > 1 { - return nil, utils.NewHandlerError(http.StatusUnprocessableEntity, fmt.Sprintf("Couldn't parse the '%s' query parameter:"+ - " expected a single value but got multiple values", param)) - } - flattenedMap[param] = valuesSlice[0] - } - return flattenedMap, nil -} - -type clientError struct { - ErrorCode int `json:"errorCode"` - ErrorMessage string `json:"errorMessage"` -} - -func sendErr(ctx *utils.APIServerContext, w http.ResponseWriter, hErr *utils.HandlerError) { - errMsg := fmt.Sprintf("got error: %s", hErr) - ctx.Warnf(errMsg) - w.WriteHeader(hErr.Code) - sendJSONResponse(w, &clientError{ - ErrorCode: hErr.Code, - ErrorMessage: hErr.ClientMessage, - }) -} - -func sendJSONResponse(w http.ResponseWriter, response interface{}) { - b, err := json.Marshal(response) - if err != nil { - panic(err) - } - _, err = fmt.Fprintf(w, string(b)) - if err != nil { - panic(err) - } -} - -func mainHandler(_ *utils.APIServerContext, routeParams map[string]string, _ map[string]string, _ []byte) (interface{}, *utils.HandlerError) { +func mainHandler(_ *httpserverutils.ServerContext, _ *http.Request, _ map[string]string, _ map[string]string, _ []byte) (interface{}, *httpserverutils.HandlerError) { return struct { Message string `json:"message"` }{ @@ -111,74 +38,74 @@ func mainHandler(_ *utils.APIServerContext, routeParams map[string]string, _ map } func addRoutes(router *mux.Router) { - router.HandleFunc("/", makeHandler(mainHandler)) + router.HandleFunc("/", httpserverutils.MakeHandler(mainHandler)) router.HandleFunc( fmt.Sprintf("/transaction/id/{%s}", routeParamTxID), - makeHandler(getTransactionByIDHandler)). + httpserverutils.MakeHandler(getTransactionByIDHandler)). Methods("GET") router.HandleFunc( fmt.Sprintf("/transaction/hash/{%s}", routeParamTxHash), - makeHandler(getTransactionByHashHandler)). + httpserverutils.MakeHandler(getTransactionByHashHandler)). Methods("GET") router.HandleFunc( fmt.Sprintf("/transactions/address/{%s}", routeParamAddress), - makeHandler(getTransactionsByAddressHandler)). + httpserverutils.MakeHandler(getTransactionsByAddressHandler)). Methods("GET") router.HandleFunc( fmt.Sprintf("/utxos/address/{%s}", routeParamAddress), - makeHandler(getUTXOsByAddressHandler)). + httpserverutils.MakeHandler(getUTXOsByAddressHandler)). Methods("GET") router.HandleFunc( fmt.Sprintf("/block/{%s}", routeParamBlockHash), - makeHandler(getBlockByHashHandler)). + httpserverutils.MakeHandler(getBlockByHashHandler)). Methods("GET") router.HandleFunc( "/blocks", - makeHandler(getBlocksHandler)). + httpserverutils.MakeHandler(getBlocksHandler)). Methods("GET") router.HandleFunc( "/fee-estimates", - makeHandler(getFeeEstimatesHandler)). + httpserverutils.MakeHandler(getFeeEstimatesHandler)). Methods("GET") router.HandleFunc( "/transaction", - makeHandler(postTransactionHandler)). + httpserverutils.MakeHandler(postTransactionHandler)). Methods("POST") } -func convertQueryParamToInt(queryParams map[string]string, param string, defaultValue int) (int, *utils.HandlerError) { +func convertQueryParamToInt(queryParams map[string]string, param string, defaultValue int) (int, *httpserverutils.HandlerError) { if _, ok := queryParams[param]; ok { intValue, err := strconv.Atoi(queryParams[param]) if err != nil { - return 0, utils.NewHandlerError(http.StatusUnprocessableEntity, fmt.Sprintf("Couldn't parse the '%s' query parameter: %s", param, err)) + return 0, httpserverutils.NewHandlerError(http.StatusUnprocessableEntity, fmt.Sprintf("Couldn't parse the '%s' query parameter: %s", param, err)) } return intValue, nil } return defaultValue, nil } -func getTransactionByIDHandler(_ *utils.APIServerContext, routeParams map[string]string, _ map[string]string, - _ []byte) (interface{}, *utils.HandlerError) { +func getTransactionByIDHandler(_ *httpserverutils.ServerContext, _ *http.Request, routeParams map[string]string, _ map[string]string, + _ []byte) (interface{}, *httpserverutils.HandlerError) { return controllers.GetTransactionByIDHandler(routeParams[routeParamTxID]) } -func getTransactionByHashHandler(_ *utils.APIServerContext, routeParams map[string]string, _ map[string]string, - _ []byte) (interface{}, *utils.HandlerError) { +func getTransactionByHashHandler(_ *httpserverutils.ServerContext, _ *http.Request, routeParams map[string]string, _ map[string]string, + _ []byte) (interface{}, *httpserverutils.HandlerError) { return controllers.GetTransactionByHashHandler(routeParams[routeParamTxHash]) } -func getTransactionsByAddressHandler(_ *utils.APIServerContext, routeParams map[string]string, queryParams map[string]string, - _ []byte) (interface{}, *utils.HandlerError) { +func getTransactionsByAddressHandler(_ *httpserverutils.ServerContext, _ *http.Request, routeParams map[string]string, queryParams map[string]string, + _ []byte) (interface{}, *httpserverutils.HandlerError) { skip, hErr := convertQueryParamToInt(queryParams, queryParamSkip, 0) if hErr != nil { @@ -192,33 +119,33 @@ func getTransactionsByAddressHandler(_ *utils.APIServerContext, routeParams map[ var err error skip, err = strconv.Atoi(queryParams[queryParamLimit]) if err != nil { - return nil, utils.NewHandlerError(http.StatusUnprocessableEntity, + return nil, httpserverutils.NewHandlerError(http.StatusUnprocessableEntity, fmt.Sprintf("Couldn't parse the '%s' query parameter: %s", queryParamLimit, err)) } } return controllers.GetTransactionsByAddressHandler(routeParams[routeParamAddress], uint64(skip), uint64(limit)) } -func getUTXOsByAddressHandler(_ *utils.APIServerContext, routeParams map[string]string, _ map[string]string, - _ []byte) (interface{}, *utils.HandlerError) { +func getUTXOsByAddressHandler(_ *httpserverutils.ServerContext, _ *http.Request, routeParams map[string]string, _ map[string]string, + _ []byte) (interface{}, *httpserverutils.HandlerError) { return controllers.GetUTXOsByAddressHandler(routeParams[routeParamAddress]) } -func getBlockByHashHandler(_ *utils.APIServerContext, routeParams map[string]string, _ map[string]string, - _ []byte) (interface{}, *utils.HandlerError) { +func getBlockByHashHandler(_ *httpserverutils.ServerContext, _ *http.Request, routeParams map[string]string, _ map[string]string, + _ []byte) (interface{}, *httpserverutils.HandlerError) { return controllers.GetBlockByHashHandler(routeParams[routeParamBlockHash]) } -func getFeeEstimatesHandler(_ *utils.APIServerContext, _ map[string]string, _ map[string]string, - _ []byte) (interface{}, *utils.HandlerError) { +func getFeeEstimatesHandler(_ *httpserverutils.ServerContext, _ *http.Request, _ map[string]string, _ map[string]string, + _ []byte) (interface{}, *httpserverutils.HandlerError) { return controllers.GetFeeEstimatesHandler() } -func getBlocksHandler(_ *utils.APIServerContext, _ map[string]string, queryParams map[string]string, - _ []byte) (interface{}, *utils.HandlerError) { +func getBlocksHandler(_ *httpserverutils.ServerContext, _ *http.Request, _ map[string]string, queryParams map[string]string, + _ []byte) (interface{}, *httpserverutils.HandlerError) { skip, hErr := convertQueryParamToInt(queryParams, queryParamSkip, 0) if hErr != nil { @@ -231,14 +158,14 @@ func getBlocksHandler(_ *utils.APIServerContext, _ map[string]string, queryParam order := defaultGetBlocksOrder if orderParamValue, ok := queryParams[queryParamOrder]; ok { if orderParamValue != controllers.OrderAscending && orderParamValue != controllers.OrderDescending { - return nil, utils.NewHandlerError(http.StatusUnprocessableEntity, fmt.Sprintf("'%s' is not a valid value for the '%s' query parameter", orderParamValue, queryParamLimit)) + return nil, httpserverutils.NewHandlerError(http.StatusUnprocessableEntity, fmt.Sprintf("'%s' is not a valid value for the '%s' query parameter", orderParamValue, queryParamLimit)) } order = orderParamValue } return controllers.GetBlocksHandler(order, uint64(skip), uint64(limit)) } -func postTransactionHandler(_ *utils.APIServerContext, _ map[string]string, _ map[string]string, - requestBody []byte) (interface{}, *utils.HandlerError) { +func postTransactionHandler(_ *httpserverutils.ServerContext, _ *http.Request, _ map[string]string, _ map[string]string, + requestBody []byte) (interface{}, *httpserverutils.HandlerError) { return nil, controllers.PostTransaction(requestBody) } diff --git a/apiserver/server/server.go b/apiserver/server/server.go index d2e1d24a0..847e9aa9d 100644 --- a/apiserver/server/server.go +++ b/apiserver/server/server.go @@ -2,6 +2,7 @@ package server import ( "context" + "github.com/daglabs/btcd/httpserverutils" "net/http" "time" @@ -15,10 +16,10 @@ const gracefulShutdownTimeout = 30 * time.Second // function to gracefully shutdown it. func Start(listenAddr string) func() { router := mux.NewRouter() - router.Use(addRequestMetadataMiddleware) - router.Use(recoveryMiddleware) - router.Use(loggingMiddleware) - router.Use(setJSONMiddleware) + router.Use(httpserverutils.AddRequestMetadataMiddleware) + router.Use(httpserverutils.RecoveryMiddleware) + router.Use(httpserverutils.LoggingMiddleware) + router.Use(httpserverutils.SetJSONMiddleware) addRoutes(router) httpServer := &http.Server{ Addr: listenAddr, diff --git a/apiserver/sync.go b/apiserver/sync.go index 50b6d4a6b..951d97089 100644 --- a/apiserver/sync.go +++ b/apiserver/sync.go @@ -6,10 +6,10 @@ import ( "fmt" "github.com/daglabs/btcd/apiserver/config" "github.com/daglabs/btcd/apiserver/database" + "github.com/daglabs/btcd/apiserver/dbmodels" "github.com/daglabs/btcd/apiserver/jsonrpc" - "github.com/daglabs/btcd/apiserver/models" - "github.com/daglabs/btcd/apiserver/utils" "github.com/daglabs/btcd/btcjson" + "github.com/daglabs/btcd/httpserverutils" "github.com/daglabs/btcd/txscript" "github.com/daglabs/btcd/util/daghash" "github.com/daglabs/btcd/util/subnetworkid" @@ -32,6 +32,7 @@ func startSync(doneChan chan struct{}) error { if err != nil { return err } + log.Infof("Finished syncing past data") // Keep the node and the API server in sync sync(client, doneChan) @@ -170,17 +171,17 @@ func findHashOfBluestBlock(mustBeChainBlock bool) (*string, error) { return nil, err } - var block models.Block + var block dbmodels.Block dbQuery := dbTx.Order("blue_score DESC") if mustBeChainBlock { - dbQuery = dbQuery.Where(&models.Block{IsChainBlock: true}) + dbQuery = dbQuery.Where(&dbmodels.Block{IsChainBlock: true}) } dbResult := dbQuery.First(&block) dbErrors := dbResult.GetErrors() - if utils.HasDBError(dbErrors) { - return nil, utils.NewErrorFromDBErrors("failed to find hash of bluest block: ", dbErrors) + if httpserverutils.HasDBError(dbErrors) { + return nil, httpserverutils.NewErrorFromDBErrors("failed to find hash of bluest block: ", dbErrors) } - if utils.IsDBRecordNotFoundError(dbErrors) { + if httpserverutils.IsDBRecordNotFoundError(dbErrors) { return nil, nil } return &block.BlockHash, nil @@ -222,15 +223,15 @@ func addBlocks(client *jsonrpc.Client, blocks []string, rawBlocks []btcjson.GetB } func doesBlockExist(dbTx *gorm.DB, blockHash string) (bool, error) { - var dbBlock models.Block + var dbBlock dbmodels.Block dbResult := dbTx. - Where(&models.Block{BlockHash: blockHash}). + Where(&dbmodels.Block{BlockHash: blockHash}). First(&dbBlock) dbErrors := dbResult.GetErrors() - if utils.HasDBError(dbErrors) { - return false, utils.NewErrorFromDBErrors("failed to find block: ", dbErrors) + if httpserverutils.HasDBError(dbErrors) { + return false, httpserverutils.NewErrorFromDBErrors("failed to find block: ", dbErrors) } - return !utils.IsDBRecordNotFoundError(dbErrors), nil + return !httpserverutils.IsDBRecordNotFoundError(dbErrors), nil } // addBlocks inserts all the data that could be gleaned out of the serialized @@ -295,12 +296,12 @@ func addBlock(client *jsonrpc.Client, block string, rawBlock btcjson.GetBlockVer return nil } -func insertBlock(dbTx *gorm.DB, rawBlock btcjson.GetBlockVerboseResult) (*models.Block, error) { +func insertBlock(dbTx *gorm.DB, rawBlock btcjson.GetBlockVerboseResult) (*dbmodels.Block, error) { bits, err := strconv.ParseUint(rawBlock.Bits, 16, 32) if err != nil { return nil, err } - dbBlock := models.Block{ + dbBlock := dbmodels.Block{ BlockHash: rawBlock.Hash, Version: rawBlock.Version, HashMerkleRoot: rawBlock.HashMerkleRoot, @@ -315,107 +316,107 @@ func insertBlock(dbTx *gorm.DB, rawBlock btcjson.GetBlockVerboseResult) (*models } dbResult := dbTx.Create(&dbBlock) dbErrors := dbResult.GetErrors() - if utils.HasDBError(dbErrors) { - return nil, utils.NewErrorFromDBErrors("failed to insert block: ", dbErrors) + if httpserverutils.HasDBError(dbErrors) { + return nil, httpserverutils.NewErrorFromDBErrors("failed to insert block: ", dbErrors) } return &dbBlock, nil } -func insertBlockParents(dbTx *gorm.DB, rawBlock btcjson.GetBlockVerboseResult, dbBlock *models.Block) error { +func insertBlockParents(dbTx *gorm.DB, rawBlock btcjson.GetBlockVerboseResult, dbBlock *dbmodels.Block) error { // Exit early if this is the genesis block if len(rawBlock.ParentHashes) == 0 { return nil } - dbWhereBlockIDsIn := make([]*models.Block, len(rawBlock.ParentHashes)) + hashesIn := make([]string, len(rawBlock.ParentHashes)) for i, parentHash := range rawBlock.ParentHashes { - dbWhereBlockIDsIn[i] = &models.Block{BlockHash: parentHash} + hashesIn[i] = parentHash } - var dbParents []models.Block + var dbParents []dbmodels.Block dbResult := dbTx. - Where(dbWhereBlockIDsIn). - First(&dbParents) + Where("block_hash in (?)", hashesIn). + Find(&dbParents) dbErrors := dbResult.GetErrors() - if utils.HasDBError(dbErrors) { - return utils.NewErrorFromDBErrors("failed to find blocks: ", dbErrors) + if httpserverutils.HasDBError(dbErrors) { + return httpserverutils.NewErrorFromDBErrors("failed to find blocks: ", dbErrors) } if len(dbParents) != len(rawBlock.ParentHashes) { return fmt.Errorf("some parents are missing for block: %s", rawBlock.Hash) } for _, dbParent := range dbParents { - dbParentBlock := models.ParentBlock{ + dbParentBlock := dbmodels.ParentBlock{ BlockID: dbBlock.ID, ParentBlockID: dbParent.ID, } dbResult := dbTx.Create(&dbParentBlock) dbErrors := dbResult.GetErrors() - if utils.HasDBError(dbErrors) { - return utils.NewErrorFromDBErrors("failed to insert parentBlock: ", dbErrors) + if httpserverutils.HasDBError(dbErrors) { + return httpserverutils.NewErrorFromDBErrors("failed to insert parentBlock: ", dbErrors) } } return nil } -func insertBlockData(dbTx *gorm.DB, block string, dbBlock *models.Block) error { +func insertBlockData(dbTx *gorm.DB, block string, dbBlock *dbmodels.Block) error { blockData, err := hex.DecodeString(block) if err != nil { return err } - dbRawBlock := models.RawBlock{ + dbRawBlock := dbmodels.RawBlock{ BlockID: dbBlock.ID, BlockData: blockData, } dbResult := dbTx.Create(&dbRawBlock) dbErrors := dbResult.GetErrors() - if utils.HasDBError(dbErrors) { - return utils.NewErrorFromDBErrors("failed to insert rawBlock: ", dbErrors) + if httpserverutils.HasDBError(dbErrors) { + return httpserverutils.NewErrorFromDBErrors("failed to insert rawBlock: ", dbErrors) } return nil } -func insertSubnetwork(dbTx *gorm.DB, transaction *btcjson.TxRawResult, client *jsonrpc.Client) (*models.Subnetwork, error) { - var dbSubnetwork models.Subnetwork +func insertSubnetwork(dbTx *gorm.DB, transaction *btcjson.TxRawResult, client *jsonrpc.Client) (*dbmodels.Subnetwork, error) { + var dbSubnetwork dbmodels.Subnetwork dbResult := dbTx. - Where(&models.Subnetwork{SubnetworkID: transaction.Subnetwork}). + Where(&dbmodels.Subnetwork{SubnetworkID: transaction.Subnetwork}). First(&dbSubnetwork) dbErrors := dbResult.GetErrors() - if utils.HasDBError(dbErrors) { - return nil, utils.NewErrorFromDBErrors("failed to find subnetwork: ", dbErrors) + if httpserverutils.HasDBError(dbErrors) { + return nil, httpserverutils.NewErrorFromDBErrors("failed to find subnetwork: ", dbErrors) } - if utils.IsDBRecordNotFoundError(dbErrors) { + if httpserverutils.IsDBRecordNotFoundError(dbErrors) { subnetwork, err := client.GetSubnetwork(transaction.Subnetwork) if err != nil { return nil, err } - dbSubnetwork = models.Subnetwork{ + dbSubnetwork = dbmodels.Subnetwork{ SubnetworkID: transaction.Subnetwork, GasLimit: subnetwork.GasLimit, } dbResult := dbTx.Create(&dbSubnetwork) dbErrors := dbResult.GetErrors() - if utils.HasDBError(dbErrors) { - return nil, utils.NewErrorFromDBErrors("failed to insert subnetwork: ", dbErrors) + if httpserverutils.HasDBError(dbErrors) { + return nil, httpserverutils.NewErrorFromDBErrors("failed to insert subnetwork: ", dbErrors) } } return &dbSubnetwork, nil } -func insertTransaction(dbTx *gorm.DB, transaction *btcjson.TxRawResult, dbSubnetwork *models.Subnetwork) (*models.Transaction, error) { - var dbTransaction models.Transaction +func insertTransaction(dbTx *gorm.DB, transaction *btcjson.TxRawResult, dbSubnetwork *dbmodels.Subnetwork) (*dbmodels.Transaction, error) { + var dbTransaction dbmodels.Transaction dbResult := dbTx. - Where(&models.Transaction{TransactionID: transaction.TxID}). + Where(&dbmodels.Transaction{TransactionID: transaction.TxID}). First(&dbTransaction) dbErrors := dbResult.GetErrors() - if utils.HasDBError(dbErrors) { - return nil, utils.NewErrorFromDBErrors("failed to find transaction: ", dbErrors) + if httpserverutils.HasDBError(dbErrors) { + return nil, httpserverutils.NewErrorFromDBErrors("failed to find transaction: ", dbErrors) } - if utils.IsDBRecordNotFoundError(dbErrors) { + if httpserverutils.IsDBRecordNotFoundError(dbErrors) { payload, err := hex.DecodeString(transaction.Payload) if err != nil { return nil, err } - dbTransaction = models.Transaction{ + dbTransaction = dbmodels.Transaction{ TransactionHash: transaction.Hash, TransactionID: transaction.TxID, LockTime: transaction.LockTime, @@ -427,38 +428,38 @@ func insertTransaction(dbTx *gorm.DB, transaction *btcjson.TxRawResult, dbSubnet } dbResult := dbTx.Create(&dbTransaction) dbErrors := dbResult.GetErrors() - if utils.HasDBError(dbErrors) { - return nil, utils.NewErrorFromDBErrors("failed to insert transaction: ", dbErrors) + if httpserverutils.HasDBError(dbErrors) { + return nil, httpserverutils.NewErrorFromDBErrors("failed to insert transaction: ", dbErrors) } } return &dbTransaction, nil } -func insertTransactionBlock(dbTx *gorm.DB, dbBlock *models.Block, dbTransaction *models.Transaction, index uint32) error { - var dbTransactionBlock models.TransactionBlock +func insertTransactionBlock(dbTx *gorm.DB, dbBlock *dbmodels.Block, dbTransaction *dbmodels.Transaction, index uint32) error { + var dbTransactionBlock dbmodels.TransactionBlock dbResult := dbTx. - Where(&models.TransactionBlock{TransactionID: dbTransaction.ID, BlockID: dbBlock.ID}). + Where(&dbmodels.TransactionBlock{TransactionID: dbTransaction.ID, BlockID: dbBlock.ID}). First(&dbTransactionBlock) dbErrors := dbResult.GetErrors() - if utils.HasDBError(dbErrors) { - return utils.NewErrorFromDBErrors("failed to find transactionBlock: ", dbErrors) + if httpserverutils.HasDBError(dbErrors) { + return httpserverutils.NewErrorFromDBErrors("failed to find transactionBlock: ", dbErrors) } - if utils.IsDBRecordNotFoundError(dbErrors) { - dbTransactionBlock = models.TransactionBlock{ + if httpserverutils.IsDBRecordNotFoundError(dbErrors) { + dbTransactionBlock = dbmodels.TransactionBlock{ TransactionID: dbTransaction.ID, BlockID: dbBlock.ID, Index: index, } dbResult := dbTx.Create(&dbTransactionBlock) dbErrors := dbResult.GetErrors() - if utils.HasDBError(dbErrors) { - return utils.NewErrorFromDBErrors("failed to insert transactionBlock: ", dbErrors) + if httpserverutils.HasDBError(dbErrors) { + return httpserverutils.NewErrorFromDBErrors("failed to insert transactionBlock: ", dbErrors) } } return nil } -func insertTransactionInputs(dbTx *gorm.DB, transaction *btcjson.TxRawResult, dbTransaction *models.Transaction) error { +func insertTransactionInputs(dbTx *gorm.DB, transaction *btcjson.TxRawResult, dbTransaction *dbmodels.Transaction) error { isCoinbase, err := isTransactionCoinbase(transaction) if err != nil { return err @@ -484,35 +485,35 @@ func isTransactionCoinbase(transaction *btcjson.TxRawResult) (bool, error) { return subnetwork.IsEqual(subnetworkid.SubnetworkIDCoinbase), nil } -func insertTransactionInput(dbTx *gorm.DB, dbTransaction *models.Transaction, input *btcjson.Vin) error { - var dbPreviousTransactionOutput models.TransactionOutput +func insertTransactionInput(dbTx *gorm.DB, dbTransaction *dbmodels.Transaction, input *btcjson.Vin) error { + var dbPreviousTransactionOutput dbmodels.TransactionOutput dbResult := dbTx. Joins("LEFT JOIN `transactions` ON `transactions`.`id` = `transaction_outputs`.`transaction_id`"). Where("`transactions`.`transactiond_id` = ? AND `transaction_outputs`.`index` = ?", input.TxID, input.Vout). First(&dbPreviousTransactionOutput) dbErrors := dbResult.GetErrors() - if utils.HasDBError(dbErrors) { - return utils.NewErrorFromDBErrors("failed to find previous transactionOutput: ", dbErrors) + if httpserverutils.HasDBError(dbErrors) { + return httpserverutils.NewErrorFromDBErrors("failed to find previous transactionOutput: ", dbErrors) } - if utils.IsDBRecordNotFoundError(dbErrors) { + if httpserverutils.IsDBRecordNotFoundError(dbErrors) { return fmt.Errorf("missing output transaction output for txID: %s and index: %d", input.TxID, input.Vout) } var dbTransactionInputCount int dbResult = dbTx. - Model(&models.TransactionInput{}). - Where(&models.TransactionInput{TransactionID: dbTransaction.ID, PreviousTransactionOutputID: dbPreviousTransactionOutput.ID}). + Model(&dbmodels.TransactionInput{}). + Where(&dbmodels.TransactionInput{TransactionID: dbTransaction.ID, PreviousTransactionOutputID: dbPreviousTransactionOutput.ID}). Count(&dbTransactionInputCount) dbErrors = dbResult.GetErrors() - if utils.HasDBError(dbErrors) { - return utils.NewErrorFromDBErrors("failed to find transactionInput: ", dbErrors) + if httpserverutils.HasDBError(dbErrors) { + return httpserverutils.NewErrorFromDBErrors("failed to find transactionInput: ", dbErrors) } if dbTransactionInputCount == 0 { scriptSig, err := hex.DecodeString(input.ScriptSig.Hex) if err != nil { return nil } - dbTransactionInput := models.TransactionInput{ + dbTransactionInput := dbmodels.TransactionInput{ TransactionID: dbTransaction.ID, PreviousTransactionOutputID: dbPreviousTransactionOutput.ID, Index: input.Vout, @@ -521,15 +522,15 @@ func insertTransactionInput(dbTx *gorm.DB, dbTransaction *models.Transaction, in } dbResult := dbTx.Create(&dbTransactionInput) dbErrors := dbResult.GetErrors() - if utils.HasDBError(dbErrors) { - return utils.NewErrorFromDBErrors("failed to insert transactionInput: ", dbErrors) + if httpserverutils.HasDBError(dbErrors) { + return httpserverutils.NewErrorFromDBErrors("failed to insert transactionInput: ", dbErrors) } } return nil } -func insertTransactionOutputs(dbTx *gorm.DB, transaction *btcjson.TxRawResult, dbTransaction *models.Transaction) error { +func insertTransactionOutputs(dbTx *gorm.DB, transaction *btcjson.TxRawResult, dbTransaction *dbmodels.Transaction) error { for _, output := range transaction.Vout { scriptPubKey, err := hex.DecodeString(output.ScriptPubKey.Hex) if err != nil { @@ -547,47 +548,47 @@ func insertTransactionOutputs(dbTx *gorm.DB, transaction *btcjson.TxRawResult, d return nil } -func insertAddress(dbTx *gorm.DB, scriptPubKey []byte) (*models.Address, error) { +func insertAddress(dbTx *gorm.DB, scriptPubKey []byte) (*dbmodels.Address, error) { _, addr, err := txscript.ExtractScriptPubKeyAddress(scriptPubKey, config.ActiveNetParams()) if err != nil { return nil, err } hexAddress := addr.EncodeAddress() - var dbAddress models.Address + var dbAddress dbmodels.Address dbResult := dbTx. - Where(&models.Address{Address: hexAddress}). + Where(&dbmodels.Address{Address: hexAddress}). First(&dbAddress) dbErrors := dbResult.GetErrors() - if utils.HasDBError(dbErrors) { - return nil, utils.NewErrorFromDBErrors("failed to find address: ", dbErrors) + if httpserverutils.HasDBError(dbErrors) { + return nil, httpserverutils.NewErrorFromDBErrors("failed to find address: ", dbErrors) } - if utils.IsDBRecordNotFoundError(dbErrors) { - dbAddress = models.Address{ + if httpserverutils.IsDBRecordNotFoundError(dbErrors) { + dbAddress = dbmodels.Address{ Address: hexAddress, } dbResult := dbTx.Create(&dbAddress) dbErrors := dbResult.GetErrors() - if utils.HasDBError(dbErrors) { - return nil, utils.NewErrorFromDBErrors("failed to insert address: ", dbErrors) + if httpserverutils.HasDBError(dbErrors) { + return nil, httpserverutils.NewErrorFromDBErrors("failed to insert address: ", dbErrors) } } return &dbAddress, nil } -func insertTransactionOutput(dbTx *gorm.DB, dbTransaction *models.Transaction, - output *btcjson.Vout, scriptPubKey []byte, dbAddress *models.Address) error { +func insertTransactionOutput(dbTx *gorm.DB, dbTransaction *dbmodels.Transaction, + output *btcjson.Vout, scriptPubKey []byte, dbAddress *dbmodels.Address) error { var dbTransactionOutputCount int dbResult := dbTx. - Model(&models.TransactionOutput{}). - Where(&models.TransactionOutput{TransactionID: dbTransaction.ID, Index: output.N}). + Model(&dbmodels.TransactionOutput{}). + Where(&dbmodels.TransactionOutput{TransactionID: dbTransaction.ID, Index: output.N}). Count(&dbTransactionOutputCount) dbErrors := dbResult.GetErrors() - if utils.HasDBError(dbErrors) { - return utils.NewErrorFromDBErrors("failed to find transactionOutput: ", dbErrors) + if httpserverutils.HasDBError(dbErrors) { + return httpserverutils.NewErrorFromDBErrors("failed to find transactionOutput: ", dbErrors) } if dbTransactionOutputCount == 0 { - dbTransactionOutput := models.TransactionOutput{ + dbTransactionOutput := dbmodels.TransactionOutput{ TransactionID: dbTransaction.ID, Index: output.N, Value: output.Value, @@ -597,8 +598,8 @@ func insertTransactionOutput(dbTx *gorm.DB, dbTransaction *models.Transaction, } dbResult := dbTx.Create(&dbTransactionOutput) dbErrors := dbResult.GetErrors() - if utils.HasDBError(dbErrors) { - return utils.NewErrorFromDBErrors("failed to insert transactionOutput: ", dbErrors) + if httpserverutils.HasDBError(dbErrors) { + return httpserverutils.NewErrorFromDBErrors("failed to insert transactionOutput: ", dbErrors) } } return nil @@ -641,29 +642,29 @@ func updateSelectedParentChain(removedChainHashes []string, addedChainBlocks []b // * The block is set IsChainBlock = false // This function will return an error if any of the above are in an unexpected state func updateRemovedChainHashes(dbTx *gorm.DB, removedHash string) error { - var dbBlock models.Block + var dbBlock dbmodels.Block dbResult := dbTx. - Where(&models.Block{BlockHash: removedHash}). + Where(&dbmodels.Block{BlockHash: removedHash}). First(&dbBlock) dbErrors := dbResult.GetErrors() - if utils.HasDBError(dbErrors) { - return utils.NewErrorFromDBErrors("failed to find block: ", dbErrors) + if httpserverutils.HasDBError(dbErrors) { + return httpserverutils.NewErrorFromDBErrors("failed to find block: ", dbErrors) } - if utils.IsDBRecordNotFoundError(dbErrors) { + if httpserverutils.IsDBRecordNotFoundError(dbErrors) { return fmt.Errorf("missing block for hash: %s", removedHash) } if !dbBlock.IsChainBlock { return fmt.Errorf("block erroneously marked as not a chain block: %s", removedHash) } - var dbTransactions []models.Transaction + var dbTransactions []dbmodels.Transaction dbResult = dbTx. - Where(&models.Transaction{AcceptingBlockID: &dbBlock.ID}). + Where(&dbmodels.Transaction{AcceptingBlockID: &dbBlock.ID}). Preload("TransactionInputs.PreviousTransactionOutput"). Find(&dbTransactions) dbErrors = dbResult.GetErrors() - if utils.HasDBError(dbErrors) { - return utils.NewErrorFromDBErrors("failed to find transactions: ", dbErrors) + if httpserverutils.HasDBError(dbErrors) { + return httpserverutils.NewErrorFromDBErrors("failed to find transactions: ", dbErrors) } for _, dbTransaction := range dbTransactions { for _, dbTransactionInput := range dbTransaction.TransactionInputs { @@ -676,24 +677,24 @@ func updateRemovedChainHashes(dbTx *gorm.DB, removedHash string) error { dbPreviousTransactionOutput.IsSpent = false dbResult = dbTx.Save(&dbPreviousTransactionOutput) dbErrors = dbResult.GetErrors() - if utils.HasDBError(dbErrors) { - return utils.NewErrorFromDBErrors("failed to update transactionOutput: ", dbErrors) + if httpserverutils.HasDBError(dbErrors) { + return httpserverutils.NewErrorFromDBErrors("failed to update transactionOutput: ", dbErrors) } } dbTransaction.AcceptingBlockID = nil dbResult := dbTx.Save(&dbTransaction) dbErrors := dbResult.GetErrors() - if utils.HasDBError(dbErrors) { - return utils.NewErrorFromDBErrors("failed to update transaction: ", dbErrors) + if httpserverutils.HasDBError(dbErrors) { + return httpserverutils.NewErrorFromDBErrors("failed to update transaction: ", dbErrors) } } dbBlock.IsChainBlock = false dbResult = dbTx.Save(&dbBlock) dbErrors = dbResult.GetErrors() - if utils.HasDBError(dbErrors) { - return utils.NewErrorFromDBErrors("failed to update block: ", dbErrors) + if httpserverutils.HasDBError(dbErrors) { + return httpserverutils.NewErrorFromDBErrors("failed to update block: ", dbErrors) } return nil @@ -707,33 +708,33 @@ func updateRemovedChainHashes(dbTx *gorm.DB, removedHash string) error { // This function will return an error if any of the above are in an unexpected state func updateAddedChainBlocks(dbTx *gorm.DB, addedBlock *btcjson.ChainBlock) error { for _, acceptedBlock := range addedBlock.AcceptedBlocks { - var dbAccepedBlock models.Block + var dbAccepedBlock dbmodels.Block dbResult := dbTx. - Where(&models.Block{BlockHash: acceptedBlock.Hash}). + Where(&dbmodels.Block{BlockHash: acceptedBlock.Hash}). First(&dbAccepedBlock) dbErrors := dbResult.GetErrors() - if utils.HasDBError(dbErrors) { - return utils.NewErrorFromDBErrors("failed to find block: ", dbErrors) + if httpserverutils.HasDBError(dbErrors) { + return httpserverutils.NewErrorFromDBErrors("failed to find block: ", dbErrors) } - if utils.IsDBRecordNotFoundError(dbErrors) { + if httpserverutils.IsDBRecordNotFoundError(dbErrors) { return fmt.Errorf("missing block for hash: %s", acceptedBlock.Hash) } if dbAccepedBlock.IsChainBlock { return fmt.Errorf("block erroneously marked as a chain block: %s", acceptedBlock.Hash) } - dbWhereTransactionIDsIn := make([]*models.Transaction, len(acceptedBlock.AcceptedTxIDs)) + transactionIDsIn := make([]string, len(acceptedBlock.AcceptedTxIDs)) for i, acceptedTxID := range acceptedBlock.AcceptedTxIDs { - dbWhereTransactionIDsIn[i] = &models.Transaction{TransactionID: acceptedTxID} + transactionIDsIn[i] = acceptedTxID } - var dbAcceptedTransactions []models.Transaction + var dbAcceptedTransactions []dbmodels.Transaction dbResult = dbTx. - Where(dbWhereTransactionIDsIn). + Where("transaction_id in (?)", transactionIDsIn). Preload("TransactionInputs.PreviousTransactionOutput"). First(&dbAcceptedTransactions) dbErrors = dbResult.GetErrors() - if utils.HasDBError(dbErrors) { - return utils.NewErrorFromDBErrors("failed to find transactions: ", dbErrors) + if httpserverutils.HasDBError(dbErrors) { + return httpserverutils.NewErrorFromDBErrors("failed to find transactions: ", dbErrors) } if len(dbAcceptedTransactions) != len(acceptedBlock.AcceptedTxIDs) { return fmt.Errorf("some transaction are missing for block: %s", acceptedBlock.Hash) @@ -750,24 +751,24 @@ func updateAddedChainBlocks(dbTx *gorm.DB, addedBlock *btcjson.ChainBlock) error dbPreviousTransactionOutput.IsSpent = true dbResult = dbTx.Save(&dbPreviousTransactionOutput) dbErrors = dbResult.GetErrors() - if utils.HasDBError(dbErrors) { - return utils.NewErrorFromDBErrors("failed to update transactionOutput: ", dbErrors) + if httpserverutils.HasDBError(dbErrors) { + return httpserverutils.NewErrorFromDBErrors("failed to update transactionOutput: ", dbErrors) } } dbAcceptedTransaction.AcceptingBlockID = &dbAccepedBlock.ID dbResult = dbTx.Save(&dbAcceptedTransaction) dbErrors = dbResult.GetErrors() - if utils.HasDBError(dbErrors) { - return utils.NewErrorFromDBErrors("failed to update transaction: ", dbErrors) + if httpserverutils.HasDBError(dbErrors) { + return httpserverutils.NewErrorFromDBErrors("failed to update transaction: ", dbErrors) } } dbAccepedBlock.IsChainBlock = true dbResult = dbTx.Save(&dbAccepedBlock) dbErrors = dbResult.GetErrors() - if utils.HasDBError(dbErrors) { - return utils.NewErrorFromDBErrors("failed to update block: ", dbErrors) + if httpserverutils.HasDBError(dbErrors) { + return httpserverutils.NewErrorFromDBErrors("failed to update block: ", dbErrors) } } return nil @@ -776,6 +777,7 @@ func updateAddedChainBlocks(dbTx *gorm.DB, addedBlock *btcjson.ChainBlock) error // handleBlockAddedMsg handles onBlockAdded messages func handleBlockAddedMsg(client *jsonrpc.Client, blockAdded *jsonrpc.BlockAddedMsg) { hash := blockAdded.Header.BlockHash() + log.Debugf("Got block %s from the RPC server", hash) block, rawBlock, err := fetchBlock(client, hash) if err != nil { log.Warnf("Could not fetch block %s: %s", hash, err) @@ -810,19 +812,20 @@ func canHandleChainChangedMsg(chainChanged *jsonrpc.ChainChangedMsg) (bool, erro } // Make sure that all the hashes exist in the database - dbWhereBlockHashesIn := make([]*models.Block, len(hashes)) + hashesIn := make([]string, len(hashes)) i := 0 for hash := range hashes { - dbWhereBlockHashesIn[i] = &models.Block{BlockHash: hash} + hashesIn[i] = hash i++ } var dbBlocksCount int dbResult := dbTx. - Where(dbWhereBlockHashesIn). + Model(&dbmodels.Block{}). + Where("block_hash in (?)", hashesIn). Count(&dbBlocksCount) dbErrors := dbResult.GetErrors() - if utils.HasDBError(dbErrors) { - return false, utils.NewErrorFromDBErrors("failed to find block count: ", dbErrors) + if httpserverutils.HasDBError(dbErrors) { + return false, httpserverutils.NewErrorFromDBErrors("failed to find block count: ", dbErrors) } if len(hashes) != dbBlocksCount { return false, nil @@ -842,7 +845,7 @@ func handleChainChangedMsg(chainChanged *jsonrpc.ChainChangedMsg) { log.Warnf("Could not update selected parent chain: %s", err) return } - log.Infof("Chain changed: removed &d blocks and added %d block", + log.Infof("Chain changed: removed %d blocks and added %d block", len(removedHashes), len(addedBlocks)) } diff --git a/blockdag/utxoset.go b/blockdag/utxoset.go index bc518cbe8..aab1bc33a 100644 --- a/blockdag/utxoset.go +++ b/blockdag/utxoset.go @@ -16,7 +16,7 @@ const ( // UnacceptedBlueScore is the blue score used for the "block" blueScore // field of the contextual transaction information provided in a // transaction store when it has not yet been accepted by a block. - UnacceptedBlueScore = math.MaxUint64 + UnacceptedBlueScore uint64 = math.MaxUint64 ) // UTXOEntry houses details about an individual transaction output in a utxo diff --git a/blockdag/validate.go b/blockdag/validate.go index cec9df146..7fcde690d 100644 --- a/blockdag/validate.go +++ b/blockdag/validate.go @@ -27,9 +27,18 @@ const ( baseSubsidy = 50 * util.SatoshiPerBitcoin // the following are used when calculating a transaction's mass - massPerTxByte = 1 - massPerScriptPubKeyByte = 10 - massPerSigOp = 10000 + + // MassPerTxByte is the number of grams that any byte + // adds to a transaction. + MassPerTxByte = 1 + + // MassPerScriptPubKeyByte is the number of grams that any + // scriptPubKey byte adds to a transaction. + MassPerScriptPubKeyByte = 10 + + // MassPerSigOp is the number of grams that any + // signature operation adds to a transaction. + MassPerSigOp = 10000 ) // isNullOutpoint determines whether or not a previous transaction outpoint @@ -125,7 +134,7 @@ func CheckTransactionSanity(tx *util.Tx, subnetworkID *subnetworkid.SubnetworkID // A transaction must not exceed the maximum allowed block mass when // serialized. serializedTxSize := msgTx.SerializeSize() - if serializedTxSize*massPerTxByte > wire.MaxMassPerTx { + if serializedTxSize*MassPerTxByte > wire.MaxMassPerTx { str := fmt.Sprintf("serialized transaction is too big - got "+ "%d, max %d", serializedTxSize, wire.MaxMassPerBlock) return ruleError(ErrTxMassTooHigh, str) @@ -361,7 +370,7 @@ func CalcTxMass(tx *util.Tx, previousScriptPubKeys [][]byte) uint64 { txSize := tx.MsgTx().SerializeSize() if tx.IsCoinBase() { - return uint64(txSize * massPerTxByte) + return uint64(txSize * MassPerTxByte) } scriptPubKeySize := 0 @@ -378,9 +387,9 @@ func CalcTxMass(tx *util.Tx, previousScriptPubKeys [][]byte) uint64 { sigOpsCount += txscript.GetPreciseSigOpCount(sigScript, previousScriptPubKeys[txInIndex], isP2SH) } - return uint64(txSize*massPerTxByte + - scriptPubKeySize*massPerScriptPubKeyByte + - sigOpsCount*massPerSigOp) + return uint64(txSize*MassPerTxByte + + scriptPubKeySize*MassPerScriptPubKeyByte + + sigOpsCount*MassPerSigOp) } // checkBlockHeaderSanity performs some preliminary checks on a block header to diff --git a/cmd/txgen/config.go b/cmd/txgen/config.go index 5e41cc67b..d52de436c 100644 --- a/cmd/txgen/config.go +++ b/cmd/txgen/config.go @@ -33,7 +33,7 @@ type config struct { TargetNumberOfInputs uint64 `long:"num-inputs" description:"Target number of transaction inputs (with some randomization)"` AveragePayloadSize uint64 `long:"payload-size" description:"Average size of transaction payload"` AverageGasFraction float64 `long:"gas-fraction" description:"The average portion of gas from the gas limit"` - AverageFeeRate uint64 `long:"fee-rate" description:"Average coins per byte fee rate"` + AverageFeeRate float64 `long:"fee-rate" description:"Average coins per gram fee rate"` } func parseConfig() (*config, error) { diff --git a/cmd/txgen/txloop.go b/cmd/txgen/txloop.go index 7c1f60d20..bc8d61d09 100644 --- a/cmd/txgen/txloop.go +++ b/cmd/txgen/txloop.go @@ -204,9 +204,13 @@ func updateWalletTxs(blockAdded *blockAddedMsg, walletTxs map[daghash.TxID]*wall } } -func randomWithAverageTarget(target uint64, allowZero bool) uint64 { +func randomWithAverageTarget(target float64) float64 { randomFraction := random.Float64() - randomNum := randomFraction * float64(target*2) + return randomFraction * target * 2 +} + +func randomIntegerWithAverageTarget(target uint64, allowZero bool) uint64 { + randomNum := randomWithAverageTarget(float64(target)) if !allowZero && randomNum < 1 { randomNum = 1 } @@ -237,17 +241,17 @@ func createRandomTxFromFunds(walletUTXOSet utxoSet, cfg *config, gasLimitMap map } if !chosenSubnetwork.IsEqual(subnetworkid.SubnetworkIDNative) { - payloadSize = randomWithAverageTarget(cfg.AveragePayloadSize, true) - gas = randomWithAverageTarget(uint64(float64(chosenGasLimit)*cfg.AverageGasFraction), true) + payloadSize = randomIntegerWithAverageTarget(cfg.AveragePayloadSize, true) + gas = randomIntegerWithAverageTarget(uint64(float64(chosenGasLimit)*cfg.AverageGasFraction), true) if gas > chosenGasLimit { gas = chosenGasLimit } } - targetNumberOfOutputs := randomWithAverageTarget(cfg.TargetNumberOfOutputs, false) - targetNumberOfInputs := randomWithAverageTarget(cfg.TargetNumberOfInputs, false) + targetNumberOfOutputs := randomIntegerWithAverageTarget(cfg.TargetNumberOfOutputs, false) + targetNumberOfInputs := randomIntegerWithAverageTarget(cfg.TargetNumberOfInputs, false) - feeRate := randomWithAverageTarget(cfg.AverageFeeRate, true) + feeRate := randomWithAverageTarget(cfg.AverageFeeRate) amount := minSpendableAmount + uint64(random.Int63n(int64(maxSpendableAmount-minSpendableAmount))) amount *= targetNumberOfOutputs @@ -280,7 +284,7 @@ func enqueueTransactions(client *txgenClient, blockAdded *blockAddedMsg, walletU return nil } -func createTx(walletUTXOSet utxoSet, minAmount uint64, feeRate uint64, targetNumberOfOutputs uint64, targetNumberOfInputs uint64, +func createTx(walletUTXOSet utxoSet, minAmount uint64, feeRate float64, targetNumberOfOutputs uint64, targetNumberOfInputs uint64, subnetworkdID *subnetworkid.SubnetworkID, payloadSize uint64, gas uint64, scriptPubKey []byte) (*wire.MsgTx, error) { var tx *wire.MsgTx if subnetworkdID.IsEqual(subnetworkid.SubnetworkIDNative) { @@ -340,9 +344,10 @@ func signTx(walletUTXOSet utxoSet, tx *wire.MsgTx) error { return nil } -func fundTx(walletUTXOSet utxoSet, tx *wire.MsgTx, amount uint64, feeRate uint64, targetNumberOfOutputs uint64, targetNumberOfInputs uint64) (uint64, error) { +func fundTx(walletUTXOSet utxoSet, tx *wire.MsgTx, amount uint64, feeRate float64, targetNumberOfOutputs uint64, targetNumberOfInputs uint64) (uint64, error) { amountSelected := uint64(0) + isTxFunded := false for outpoint, output := range walletUTXOSet { amountSelected += output.Value @@ -353,29 +358,30 @@ func fundTx(walletUTXOSet utxoSet, tx *wire.MsgTx, amount uint64, feeRate uint64 // Check if transaction has enough funds. If we don't have enough // coins from he current amount selected to pay the fee, or we have // less inputs then the targeted amount, continue to grab more coins. - if uint64(len(tx.TxIn)) >= targetNumberOfInputs && isFunded(tx, feeRate, targetNumberOfOutputs, amountSelected, amount, walletUTXOSet) { + isTxFunded = isFunded(tx, feeRate, targetNumberOfOutputs, amountSelected, amount, walletUTXOSet) + if uint64(len(tx.TxIn)) >= targetNumberOfInputs && isTxFunded { break } } - if !isFunded(tx, feeRate, targetNumberOfOutputs, amountSelected, amount, walletUTXOSet) { + if !isTxFunded { return 0, fmt.Errorf("not enough funds for coin selection") } return amountSelected, nil } -// Check if the transaction has enough funds to cover the fee +// isFunded checks if the transaction has enough funds to cover the fee // required for the txn. -func isFunded(tx *wire.MsgTx, feeRate uint64, targetNumberOfOutputs uint64, amountSelected uint64, targetAmount uint64, walletUTXOSet utxoSet) bool { +func isFunded(tx *wire.MsgTx, feeRate float64, targetNumberOfOutputs uint64, amountSelected uint64, targetAmount uint64, walletUTXOSet utxoSet) bool { reqFee := calcFee(tx, feeRate, targetNumberOfOutputs, walletUTXOSet) return amountSelected > reqFee && amountSelected-reqFee >= targetAmount } -func calcFee(msgTx *wire.MsgTx, feeRate uint64, numberOfOutputs uint64, walletUTXOSet utxoSet) uint64 { +func calcFee(msgTx *wire.MsgTx, feeRate float64, numberOfOutputs uint64, walletUTXOSet utxoSet) uint64 { txMass := calcTxMass(msgTx, walletUTXOSet) txMassWithOutputs := txMass + outputsTotalSize(numberOfOutputs) - reqFee := txMassWithOutputs * feeRate + reqFee := uint64(float64(txMassWithOutputs) * feeRate) if reqFee < minTxFee { return minTxFee } diff --git a/config/config.go b/config/config.go index dd334254f..8faf02445 100644 --- a/config/config.go +++ b/config/config.go @@ -281,7 +281,7 @@ func LoadAndSetMainConfig() error { return nil } -//MainConfig is a getter to the main config +// MainConfig is a getter to the main config func MainConfig() *Config { return mainCfg } diff --git a/faucet/config/config.go b/faucet/config/config.go new file mode 100644 index 000000000..6ad412c46 --- /dev/null +++ b/faucet/config/config.go @@ -0,0 +1,112 @@ +package config + +import ( + "github.com/daglabs/btcd/apiserver/logger" + "github.com/daglabs/btcd/dagconfig" + "github.com/daglabs/btcd/util" + "github.com/jessevdk/go-flags" + "github.com/pkg/errors" + "path/filepath" +) + +const ( + defaultLogFilename = "faucet.log" + defaultErrLogFilename = "faucet_err.log" +) + +var ( + // Default configuration options + defaultLogDir = util.AppDataDir("faucet", false) + defaultDBAddress = "localhost:3306" + defaultHTTPListen = "0.0.0.0:8081" + + // activeNetParams are the currently active net params + activeNetParams *dagconfig.Params +) + +// Config defines the configuration options for the API server. +type Config struct { + LogDir string `long:"logdir" description:"Directory to log output."` + HTTPListen string `long:"listen" description:"HTTP address to listen on (default: 0.0.0.0:8081)"` + APIServerURL string `long:"api-server-url" description:"The API server url to connect to" required:"true"` + PrivateKey string `long:"private-key" description:"Faucet Private key" required:"true"` + DBAddress string `long:"dbaddress" description:"Database address"` + DBUser string `long:"dbuser" description:"Database user" required:"true"` + DBPassword string `long:"dbpass" description:"Database password" required:"true"` + DBName string `long:"dbname" description:"Database name" required:"true"` + Migrate bool `long:"migrate" description:"Migrate the database to the latest version. The server will not start when using this flag."` + FeeRate float64 `long:"fee-rate" description:"Coins per gram fee rate"` + TestNet bool `long:"testnet" description:"Connect to testnet"` + SimNet bool `long:"simnet" description:"Connect to the simulation test network"` + DevNet bool `long:"devnet" description:"Connect to the development test network"` +} + +var cfg *Config + +// Parse parses the CLI arguments and returns a config struct. +func Parse() error { + cfg = &Config{ + LogDir: defaultLogDir, + DBAddress: defaultDBAddress, + HTTPListen: defaultHTTPListen, + } + parser := flags.NewParser(cfg, flags.PrintErrors|flags.HelpFlag) + _, err := parser.Parse() + if err != nil { + return err + } + + err = resolveNetwork(cfg) + if err != nil { + return err + } + + logFile := filepath.Join(cfg.LogDir, defaultLogFilename) + errLogFile := filepath.Join(cfg.LogDir, defaultErrLogFilename) + logger.InitLog(logFile, errLogFile) + + return nil +} + +func resolveNetwork(cfg *Config) error { + // Multiple networks can't be selected simultaneously. + numNets := 0 + if cfg.TestNet { + numNets++ + } + if cfg.SimNet { + numNets++ + } + if cfg.DevNet { + numNets++ + } + if numNets > 1 { + return errors.New("multiple net params (testnet, simnet, devnet, etc.) can't be used " + + "together -- choose one of them") + } + + activeNetParams = &dagconfig.MainNetParams + switch { + case cfg.TestNet: + activeNetParams = &dagconfig.TestNetParams + case cfg.SimNet: + activeNetParams = &dagconfig.SimNetParams + case cfg.DevNet: + activeNetParams = &dagconfig.DevNetParams + } + + return nil +} + +// MainConfig is a getter to the main config +func MainConfig() (*Config, error) { + if cfg == nil { + return nil, errors.New("No configuration was set for the faucet") + } + return cfg, nil +} + +// ActiveNetParams returns the currently active net params +func ActiveNetParams() *dagconfig.Params { + return activeNetParams +} diff --git a/faucet/database/database.go b/faucet/database/database.go new file mode 100644 index 000000000..4e669725b --- /dev/null +++ b/faucet/database/database.go @@ -0,0 +1,150 @@ +package database + +import ( + "errors" + "fmt" + "os" + + "github.com/daglabs/btcd/faucet/config" + "github.com/golang-migrate/migrate/v4/source" + "github.com/jinzhu/gorm" + + "github.com/golang-migrate/migrate/v4" +) + +// db is the API server database. +var db *gorm.DB + +// DB returns a reference to the database connection +func DB() (*gorm.DB, error) { + if db == nil { + return nil, errors.New("Database is not connected") + } + return db, nil +} + +type gormLogger struct{} + +func (l gormLogger) Print(v ...interface{}) { + str := fmt.Sprint(v...) + log.Errorf(str) +} + +// Connect connects to the database mentioned in +// config variable. +func Connect() error { + connectionString, err := buildConnectionString() + if err != nil { + return err + } + migrator, driver, err := openMigrator(connectionString) + if err != nil { + return err + } + isCurrent, version, err := isCurrent(migrator, driver) + if err != nil { + return fmt.Errorf("Error checking whether the database is current: %s", err) + } + if !isCurrent { + return fmt.Errorf("Database is not current (version %d). Please migrate"+ + " the database by running the faucet with --migrate flag and then run it again.", version) + } + + db, err = gorm.Open("mysql", connectionString) + if err != nil { + return err + } + + db.SetLogger(gormLogger{}) + return nil +} + +// Close closes the connection to the database +func Close() error { + if db == nil { + return nil + } + err := db.Close() + db = nil + return err +} + +func buildConnectionString() (string, error) { + cfg, err := config.MainConfig() + if err != nil { + return "", err + } + return fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8&parseTime=True", + cfg.DBUser, cfg.DBPassword, cfg.DBAddress, cfg.DBName), nil +} + +// isCurrent resolves whether the database is on the latest +// version of the schema. +func isCurrent(migrator *migrate.Migrate, driver source.Driver) (bool, uint, error) { + // Get the current version + version, isDirty, err := migrator.Version() + if err == migrate.ErrNilVersion { + return false, 0, nil + } + if err != nil { + return false, 0, err + } + if isDirty { + return false, 0, fmt.Errorf("Database is dirty") + } + + // The database is current if Next returns ErrNotExist + _, err = driver.Next(version) + if pathErr, ok := err.(*os.PathError); ok { + if pathErr.Err == os.ErrNotExist { + return true, version, nil + } + } + return false, version, err +} + +func openMigrator(connectionString string) (*migrate.Migrate, source.Driver, error) { + driver, err := source.Open("file://migrations") + if err != nil { + return nil, nil, err + } + migrator, err := migrate.NewWithSourceInstance( + "migrations", driver, "mysql://"+connectionString) + if err != nil { + return nil, nil, err + } + return migrator, driver, nil +} + +// Migrate database to the latest version. +func Migrate() error { + connectionString, err := buildConnectionString() + if err != nil { + return err + } + migrator, driver, err := openMigrator(connectionString) + if err != nil { + return err + } + isCurrent, version, err := isCurrent(migrator, driver) + if err != nil { + return fmt.Errorf("Error checking whether the database is current: %s", err) + } + if isCurrent { + log.Infof("Database is already up-to-date (version %d)", version) + return nil + } + err = migrator.Up() + if err != nil { + return err + } + version, isDirty, err := migrator.Version() + if err != nil { + return err + } + if isDirty { + return fmt.Errorf("error migrating database: database is dirty") + } + log.Infof("Migrated database to the latest version (version %d)", version) + return nil +} diff --git a/faucet/database/log.go b/faucet/database/log.go new file mode 100644 index 000000000..05343395a --- /dev/null +++ b/faucet/database/log.go @@ -0,0 +1,9 @@ +package database + +import "github.com/daglabs/btcd/util/panics" +import "github.com/daglabs/btcd/apiserver/logger" + +var ( + log = logger.BackendLog.Logger("DTBS") + spawn = panics.GoroutineWrapperFunc(log, logger.BackendLog) +) diff --git a/faucet/faucet.go b/faucet/faucet.go new file mode 100644 index 000000000..a7822e83a --- /dev/null +++ b/faucet/faucet.go @@ -0,0 +1,331 @@ +package main + +import ( + "bytes" + "encoding/hex" + "encoding/json" + "fmt" + "github.com/daglabs/btcd/apiserver/apimodels" + "github.com/daglabs/btcd/blockdag" + "github.com/daglabs/btcd/faucet/config" + "github.com/daglabs/btcd/httpserverutils" + "github.com/daglabs/btcd/txscript" + "github.com/daglabs/btcd/util" + "github.com/daglabs/btcd/util/daghash" + "github.com/daglabs/btcd/wire" + "github.com/pkg/errors" + "io/ioutil" + "net/http" + "net/url" + "path" +) + +const ( + sendAmount = 10000 + // Value 8 bytes + serialized varint size for the length of ScriptPubKey + + // ScriptPubKey bytes. + outputSize uint64 = 8 + 1 + 25 + minTxFee uint64 = 3000 + + requiredConfirmations = 10 +) + +type utxoSet map[wire.Outpoint]*blockdag.UTXOEntry + +// apiURL returns a full concatenated URL from the base +// API server URL and the given path. +func apiURL(requestPath string) (string, error) { + cfg, err := config.MainConfig() + if err != nil { + return "", err + } + u, err := url.Parse(cfg.APIServerURL) + if err != nil { + return "", errors.WithStack(err) + } + u.Path = path.Join(u.Path, requestPath) + return u.String(), nil +} + +// getFromAPIServer makes an HTTP GET request to the API server +// to the given request path, and returns the response body. +func getFromAPIServer(requestPath string) ([]byte, error) { + getAPIURL, err := apiURL(requestPath) + if err != nil { + return nil, err + } + resp, err := http.Get(getAPIURL) + if err != nil { + return nil, errors.WithStack(err) + } + defer func() { + err := resp.Body.Close() + if err != nil { + panic(errors.WithStack(err)) + } + }() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, errors.WithStack(err) + } + if resp.StatusCode != http.StatusOK { + clientError := &httpserverutils.ClientError{} + err := json.Unmarshal(body, &clientError) + if err != nil { + return nil, errors.WithStack(err) + } + return nil, errors.WithStack(clientError) + } + return body, nil +} + +// getFromAPIServer makes an HTTP POST request to the API server +// to the given request path. It converts the given data to JSON, +// and post it as the POST data. +func postToAPIServer(requestPath string, data interface{}) error { + dataBytes, err := json.Marshal(data) + if err != nil { + return errors.WithStack(err) + } + r := bytes.NewReader(dataBytes) + postAPIURL, err := apiURL(requestPath) + if err != nil { + return err + } + resp, err := http.Post(postAPIURL, "application/json", r) + if err != nil { + return errors.WithStack(err) + } + defer func() { + err := resp.Body.Close() + if err != nil { + panic(errors.WithStack(err)) + } + }() + if resp.StatusCode != http.StatusOK { + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return errors.WithStack(err) + } + clientError := &httpserverutils.ClientError{} + err = json.Unmarshal(body, &clientError) + if err != nil { + return errors.WithStack(err) + } + return errors.WithStack(clientError) + } + return nil +} + +func isUTXOMatured(entry *blockdag.UTXOEntry, confirmations uint64) bool { + if entry.IsCoinbase() { + return confirmations >= config.ActiveNetParams().BlockCoinbaseMaturity + } + return confirmations >= requiredConfirmations +} + +func getWalletUTXOSet() (utxoSet, error) { + body, err := getFromAPIServer(fmt.Sprintf("utxos/address/%s", faucetAddress.EncodeAddress())) + if err != nil { + return nil, err + } + utxoResponses := []*apimodels.TransactionOutputResponse{} + err = json.Unmarshal(body, &utxoResponses) + if err != nil { + return nil, err + } + walletUTXOSet := make(utxoSet) + for _, utxoResponse := range utxoResponses { + scriptPubKey, err := hex.DecodeString(utxoResponse.ScriptPubKey) + if err != nil { + return nil, err + } + txOut := &wire.TxOut{ + Value: utxoResponse.Value, + ScriptPubKey: scriptPubKey, + } + txID, err := daghash.NewTxIDFromStr(utxoResponse.TransactionID) + if err != nil { + return nil, err + } + outpoint := wire.NewOutpoint(txID, utxoResponse.Index) + utxoEntry := blockdag.NewUTXOEntry(txOut, *utxoResponse.IsCoinbase, utxoResponse.AcceptingBlockBlueScore) + if !isUTXOMatured(utxoEntry, *utxoResponse.Confirmations) { + continue + } + walletUTXOSet[*outpoint] = utxoEntry + } + return walletUTXOSet, nil +} + +func sendToAddress(address util.Address) (*wire.MsgTx, error) { + tx, err := createTx(address) + if err != nil { + return nil, err + } + buf := bytes.NewBuffer(make([]byte, 0, tx.SerializeSize())) + if err := tx.Serialize(buf); err != nil { + return nil, err + } + rawTx := &apimodels.RawTransaction{RawTransaction: hex.EncodeToString(buf.Bytes())} + return tx, postToAPIServer("transaction", rawTx) +} + +func createTx(address util.Address) (*wire.MsgTx, error) { + walletUTXOSet, err := getWalletUTXOSet() + if err != nil { + return nil, err + } + tx, err := createUnsignedTx(walletUTXOSet, address) + if err != nil { + return nil, err + } + err = signTx(walletUTXOSet, tx) + if err != nil { + return nil, err + } + return tx, nil +} + +func createUnsignedTx(walletUTXOSet utxoSet, address util.Address) (*wire.MsgTx, error) { + tx := wire.NewNativeMsgTx(wire.TxVersion, nil, nil) + netAmount, isChangeOutputRequired, err := fundTx(walletUTXOSet, tx, sendAmount) + if err != nil { + return nil, err + } + if isChangeOutputRequired { + tx.AddTxOut(&wire.TxOut{ + Value: sendAmount, + ScriptPubKey: address.ScriptAddress(), + }) + tx.AddTxOut(&wire.TxOut{ + Value: netAmount - sendAmount, + ScriptPubKey: faucetScriptPubKey, + }) + return tx, nil + } + tx.AddTxOut(&wire.TxOut{ + Value: netAmount, + ScriptPubKey: address.ScriptAddress(), + }) + return tx, nil +} + +// signTx signs a transaction +func signTx(walletUTXOSet utxoSet, tx *wire.MsgTx) error { + for i, txIn := range tx.TxIn { + outpoint := txIn.PreviousOutpoint + + sigScript, err := txscript.SignatureScript(tx, i, walletUTXOSet[outpoint].ScriptPubKey(), + txscript.SigHashAll, faucetPrivateKey, true) + if err != nil { + return fmt.Errorf("Failed to sign transaction: %s", err) + } + txIn.SignatureScript = sigScript + } + + return nil +} + +func fundTx(walletUTXOSet utxoSet, tx *wire.MsgTx, amount uint64) (netAmount uint64, isChangeOutputRequired bool, err error) { + amountSelected := uint64(0) + isTxFunded := false + for outpoint, entry := range walletUTXOSet { + amountSelected += entry.Amount() + + // Add the selected output to the transaction + tx.AddTxIn(wire.NewTxIn(&outpoint, nil)) + + // Check if transaction has enough funds. If we don't have enough + // coins from the current amount selected to pay the fee continue + // to grab more coins. + isTxFunded, isChangeOutputRequired, netAmount, err = isFundedAndIsChangeOutputRequired(tx, amountSelected, amount, walletUTXOSet) + if err != nil { + return 0, false, err + } + if isTxFunded { + break + } + } + + if !isTxFunded { + return 0, false, errors.Errorf("not enough funds for coin selection") + } + + return netAmount, isChangeOutputRequired, nil +} + +// isFundedAndIsChangeOutputRequired returns three values and an error: +// * isTxFunded is whether the transaction inputs cover the target amount + the required fee. +// * isChangeOutputRequired is whether it is profitable to add an additional change +// output to the transaction. +// * netAmount is the amount of coins that will be eventually sent to the recipient. If no +// change output is needed, the netAmount will be usually a little bit higher than the +// targetAmount. Otherwise, it'll be the same as the targetAmount. +func isFundedAndIsChangeOutputRequired(tx *wire.MsgTx, amountSelected uint64, targetAmount uint64, walletUTXOSet utxoSet) (isTxFunded, isChangeOutputRequired bool, netAmount uint64, err error) { + // First check if it can be funded with one output and the required fee for it. + isFundedWithOneOutput, oneOutputFee, err := isFundedWithNumberOfOutputs(tx, 1, amountSelected, targetAmount, walletUTXOSet) + if err != nil { + return false, false, 0, err + } + if !isFundedWithOneOutput { + return false, false, 0, nil + } + + // Now check if it can be funded with two outputs and the required fee for it. + isFundedWithTwoOutputs, twoOutputsFee, err := isFundedWithNumberOfOutputs(tx, 2, amountSelected, targetAmount, walletUTXOSet) + if err != nil { + return false, false, 0, err + } + + // If it can be funded with two outputs, check if adding a change output worth it: i.e. check if + // the amount you save by not sending the recipient the whole inputs amount (minus fees) is greater + // than the additional fee that is required by adding a change output. If this is the case, return + // isChangeOutputRequired as true. + if isFundedWithTwoOutputs && twoOutputsFee-oneOutputFee < targetAmount-amountSelected { + return true, true, amountSelected - twoOutputsFee, nil + } + return true, false, amountSelected - oneOutputFee, nil +} + +// isFundedWithNumberOfOutputs returns whether the transaction inputs cover +// the target amount + the required fee with the assumed number of outputs. +func isFundedWithNumberOfOutputs(tx *wire.MsgTx, numberOfOutputs uint64, amountSelected uint64, targetAmount uint64, walletUTXOSet utxoSet) (isTxFunded bool, fee uint64, err error) { + reqFee, err := calcFee(tx, numberOfOutputs, walletUTXOSet) + if err != nil { + return false, 0, err + } + return amountSelected > reqFee && amountSelected-reqFee >= targetAmount, reqFee, nil +} + +func calcFee(msgTx *wire.MsgTx, numberOfOutputs uint64, walletUTXOSet utxoSet) (uint64, error) { + txMass := calcTxMass(msgTx, walletUTXOSet) + txMassWithOutputs := txMass + outputsTotalSize(numberOfOutputs)*blockdag.MassPerTxByte + cfg, err := config.MainConfig() + if err != nil { + return 0, err + } + reqFee := uint64(float64(txMassWithOutputs) * cfg.FeeRate) + if reqFee < minTxFee { + return minTxFee, nil + } + return reqFee, nil +} + +func outputsTotalSize(numberOfOutputs uint64) uint64 { + return numberOfOutputs*outputSize + uint64(wire.VarIntSerializeSize(numberOfOutputs)) +} + +func calcTxMass(msgTx *wire.MsgTx, walletUTXOSet utxoSet) uint64 { + previousScriptPubKeys := getPreviousScriptPubKeys(msgTx, walletUTXOSet) + return blockdag.CalcTxMass(util.NewTx(msgTx), previousScriptPubKeys) +} + +func getPreviousScriptPubKeys(msgTx *wire.MsgTx, walletUTXOSet utxoSet) [][]byte { + previousScriptPubKeys := make([][]byte, len(msgTx.TxIn)) + for i, txIn := range msgTx.TxIn { + outpoint := txIn.PreviousOutpoint + previousScriptPubKeys[i] = walletUTXOSet[outpoint].ScriptPubKey() + } + return previousScriptPubKeys +} diff --git a/faucet/ip_usage.go b/faucet/ip_usage.go new file mode 100644 index 000000000..59cae4a94 --- /dev/null +++ b/faucet/ip_usage.go @@ -0,0 +1,65 @@ +package main + +import ( + "github.com/daglabs/btcd/faucet/database" + "github.com/daglabs/btcd/httpserverutils" + "net" + "net/http" + "time" +) + +const minRequestInterval = time.Hour * 24 + +type ipUse struct { + IP string + LastUse time.Time +} + +func ipFromRequest(r *http.Request) (string, error) { + ip, _, err := net.SplitHostPort(r.RemoteAddr) + if err != nil { + return "", err + } + return ip, nil +} + +func validateIPUsage(r *http.Request) *httpserverutils.HandlerError { + db, err := database.DB() + if err != nil { + return httpserverutils.NewInternalServerHandlerError(err.Error()) + } + now := time.Now() + timeBeforeMinRequestInterval := now.Add(-minRequestInterval) + var count int + ip, err := ipFromRequest(r) + if err != nil { + return httpserverutils.NewInternalServerHandlerError(err.Error()) + } + dbResult := db.Model(&ipUse{}).Where(&ipUse{IP: ip}).Where("last_use BETWEEN ? AND ?", timeBeforeMinRequestInterval, now).Count(&count) + dbErrors := dbResult.GetErrors() + if httpserverutils.HasDBError(dbErrors) { + return httpserverutils.NewHandlerErrorFromDBErrors("Some errors were encountered when checking the last use of an IP:", dbResult.GetErrors()) + } + if count != 0 { + return httpserverutils.NewHandlerError(http.StatusForbidden, "A user is allowed to to have one request from the faucet every 24 hours.") + } + return nil +} + +func updateIPUsage(r *http.Request) *httpserverutils.HandlerError { + db, err := database.DB() + if err != nil { + return httpserverutils.NewInternalServerHandlerError(err.Error()) + } + + ip, err := ipFromRequest(r) + if err != nil { + return httpserverutils.NewInternalServerHandlerError(err.Error()) + } + dbResult := db.Where(&ipUse{IP: ip}).Assign(&ipUse{LastUse: time.Now()}).FirstOrCreate(&ipUse{}) + dbErrors := dbResult.GetErrors() + if httpserverutils.HasDBError(dbErrors) { + return httpserverutils.NewHandlerErrorFromDBErrors("Some errors were encountered when upserting the IP to the new date:", dbResult.GetErrors()) + } + return nil +} diff --git a/faucet/log.go b/faucet/log.go new file mode 100644 index 000000000..749aca430 --- /dev/null +++ b/faucet/log.go @@ -0,0 +1,11 @@ +package main + +import ( + "github.com/daglabs/btcd/logger" + "github.com/daglabs/btcd/util/panics" +) + +var ( + log = logger.BackendLog.Logger("FAUC") + spawn = panics.GoroutineWrapperFunc(log, logger.BackendLog) +) diff --git a/faucet/main.go b/faucet/main.go new file mode 100644 index 000000000..a64a2a7b8 --- /dev/null +++ b/faucet/main.go @@ -0,0 +1,89 @@ +package main + +import ( + "fmt" + "github.com/daglabs/btcd/btcec" + "github.com/daglabs/btcd/dagconfig" + "github.com/daglabs/btcd/faucet/config" + "github.com/daglabs/btcd/faucet/database" + "github.com/daglabs/btcd/txscript" + "github.com/daglabs/btcd/util" + "github.com/daglabs/btcd/util/base58" + "github.com/pkg/errors" + "os" + + "github.com/daglabs/btcd/logger" + "github.com/daglabs/btcd/signal" + "github.com/daglabs/btcd/util/panics" + _ "github.com/golang-migrate/migrate/v4/database/mysql" + _ "github.com/golang-migrate/migrate/v4/source/file" + _ "github.com/jinzhu/gorm/dialects/mysql" +) + +var ( + faucetAddress util.Address + faucetPrivateKey *btcec.PrivateKey + faucetScriptPubKey []byte +) + +func main() { + defer panics.HandlePanic(log, logger.BackendLog) + + err := config.Parse() + if err != nil { + err := errors.Wrap(err, "Error parsing command-line arguments") + _, err = fmt.Fprintf(os.Stderr, err.Error()) + if err != nil { + panic(err) + } + return + } + + cfg, err := config.MainConfig() + if err != nil { + panic(err) + } + + if cfg.Migrate { + err := database.Migrate() + if err != nil { + panic(fmt.Errorf("Error migrating database: %s", err)) + } + return + } + + err = database.Connect() + if err != nil { + panic(fmt.Errorf("Error connecting to database: %s", err)) + } + defer func() { + err := database.Close() + if err != nil { + panic(fmt.Errorf("Error closing the database: %s", err)) + } + }() + + privateKeyBytes := base58.Decode(cfg.PrivateKey) + faucetPrivateKey, _ = btcec.PrivKeyFromBytes(btcec.S256(), privateKeyBytes) + + faucetAddress, err = privateKeyToP2PKHAddress(faucetPrivateKey, config.ActiveNetParams()) + if err != nil { + panic(fmt.Errorf("Failed to get P2PKH address from private key: %s", err)) + } + + faucetScriptPubKey, err = txscript.PayToAddrScript(faucetAddress) + if err != nil { + panic(fmt.Errorf("failed to generate faucetScriptPubKey to address: %s", err)) + } + + shutdownServer := startHTTPServer(cfg.HTTPListen) + defer shutdownServer() + + interrupt := signal.InterruptListener() + <-interrupt +} + +// privateKeyToP2PKHAddress generates p2pkh address from private key. +func privateKeyToP2PKHAddress(key *btcec.PrivateKey, net *dagconfig.Params) (util.Address, error) { + return util.NewAddressPubKeyHashFromPublicKey(key.PubKey().SerializeCompressed(), net.Prefix) +} diff --git a/faucet/migrations/000001_create_ip_uses_table.down.sql b/faucet/migrations/000001_create_ip_uses_table.down.sql new file mode 100644 index 000000000..0ee7d0b52 --- /dev/null +++ b/faucet/migrations/000001_create_ip_uses_table.down.sql @@ -0,0 +1 @@ +DROP TABLE `ip_uses`; diff --git a/faucet/migrations/000001_create_ip_uses_table.up.sql b/faucet/migrations/000001_create_ip_uses_table.up.sql new file mode 100644 index 000000000..4dc324ce0 --- /dev/null +++ b/faucet/migrations/000001_create_ip_uses_table.up.sql @@ -0,0 +1,6 @@ +CREATE TABLE `ip_uses` +( + `ip` VARCHAR(39) NOT NULL, + `last_use` DATETIME NOT NULL, + PRIMARY KEY (`ip`) +); \ No newline at end of file diff --git a/faucet/server.go b/faucet/server.go new file mode 100644 index 000000000..4372645bb --- /dev/null +++ b/faucet/server.go @@ -0,0 +1,81 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "github.com/daglabs/btcd/faucet/config" + "github.com/daglabs/btcd/httpserverutils" + "github.com/daglabs/btcd/util" + "net/http" + "time" + + "github.com/gorilla/handlers" + "github.com/gorilla/mux" +) + +const gracefulShutdownTimeout = 30 * time.Second + +// startHTTPServer starts the HTTP REST server and returns a +// function to gracefully shutdown it. +func startHTTPServer(listenAddr string) func() { + router := mux.NewRouter() + router.Use(httpserverutils.AddRequestMetadataMiddleware) + router.Use(httpserverutils.RecoveryMiddleware) + router.Use(httpserverutils.LoggingMiddleware) + router.Use(httpserverutils.SetJSONMiddleware) + router.HandleFunc( + "/request_money", + httpserverutils.MakeHandler(requestMoneyHandler)). + Methods("POST") + httpServer := &http.Server{ + Addr: listenAddr, + Handler: handlers.CORS()(router), + } + spawn(func() { + log.Errorf("%s", httpServer.ListenAndServe()) + }) + + return func() { + ctx, cancel := context.WithTimeout(context.Background(), gracefulShutdownTimeout) + defer cancel() + err := httpServer.Shutdown(ctx) + if err != nil { + log.Errorf("Error shutting down HTTP server: %s", err) + } + } +} + +type requestMoneyData struct { + Address string `json:"address"` +} + +func requestMoneyHandler(_ *httpserverutils.ServerContext, r *http.Request, _ map[string]string, _ map[string]string, + requestBody []byte) (interface{}, *httpserverutils.HandlerError) { + hErr := validateIPUsage(r) + if hErr != nil { + return nil, hErr + } + requestData := &requestMoneyData{} + err := json.Unmarshal(requestBody, requestData) + if err != nil { + return nil, httpserverutils.NewHandlerErrorWithCustomClientMessage(http.StatusUnprocessableEntity, + fmt.Sprintf("Error unmarshalling request body: %s", err), + "The request body is not json-formatted") + } + address, err := util.DecodeAddress(requestData.Address, config.ActiveNetParams().Prefix) + if err != nil { + return nil, httpserverutils.NewHandlerErrorWithCustomClientMessage(http.StatusUnprocessableEntity, + fmt.Sprintf("Error decoding address: %s", err), + "Error decoding address") + } + tx, err := sendToAddress(address) + if err != nil { + return nil, httpserverutils.NewInternalServerHandlerError(err.Error()) + } + hErr = updateIPUsage(r) + if hErr != nil { + return nil, hErr + } + return tx.TxID().String(), nil +} diff --git a/go.mod b/go.mod index d20917cad..e43c6b04e 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.12 require ( bou.ke/monkey v1.0.1 github.com/aead/siphash v1.0.1 + github.com/aws/aws-lambda-go v1.13.2 github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd github.com/btcsuite/goleveldb v1.0.0 github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 @@ -18,5 +19,6 @@ require ( github.com/jrick/logrotate v1.0.0 github.com/kkdai/bstream v0.0.0-20181106074824-b3251f7901ec github.com/miekg/dns v1.1.6 + github.com/pkg/errors v0.8.1 golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734 ) diff --git a/go.sum b/go.sum index 59c72e4c7..6b493b542 100644 --- a/go.sum +++ b/go.sum @@ -17,6 +17,8 @@ github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBA github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/aws/aws-lambda-go v1.13.2 h1:8lYuRVn6rESoUNZXdbCmtGB4bBk4vcVYojiHjE4mMrM= +github.com/aws/aws-lambda-go v1.13.2/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= github.com/aws/aws-sdk-go v1.17.7/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k= @@ -36,6 +38,7 @@ github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMe github.com/cockroachdb/cockroach-go v0.0.0-20181001143604-e0a95dfd547c/go.mod h1:XGLbWH/ujMcbPbhZq52Nv6UrCghb1yGn//133kEsvDk= github.com/containerd/containerd v1.2.7 h1:8lqLbl7u1j3MmiL9cJ/O275crSq7bfwUayvvatEupQk= github.com/containerd/containerd v1.2.7/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cznic/b v0.0.0-20180115125044-35e9bbe41f07/go.mod h1:URriBxXwVq5ijiJ12C7iIZqlA69nTlI+LgI6/pwftG8= github.com/cznic/fileutil v0.0.0-20180108211300-6a051e75936f/go.mod h1:8S58EK26zhXSxzv7NQFpnliaOQsmDUxvoQO3rt154Vg= github.com/cznic/golex v0.0.0-20170803123110-4ab7c5e190e4/go.mod h1:+bmmJDNmKlhWNG+gwWCkaBoTy39Fs+bzRxVBzoTQbIc= @@ -185,8 +188,10 @@ github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y8 github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.1 h1:GL2rEmy6nsikmW0r8opw9JIRScdMF5hA8cOYLH7In1k= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= @@ -195,7 +200,10 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/tidwall/pretty v0.0.0-20180105212114-65a9db5fad51/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/xanzy/go-gitlab v0.15.0/go.mod h1:8zdQa/ri1dfn8eS3Ir1SyfvOKlw7WBJ8DVThkpGiXrs= github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I= github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y= diff --git a/apiserver/utils/context.go b/httpserverutils/context.go similarity index 56% rename from apiserver/utils/context.go rename to httpserverutils/context.go index 923a246cc..7c04cce92 100644 --- a/apiserver/utils/context.go +++ b/httpserverutils/context.go @@ -1,4 +1,4 @@ -package utils +package httpserverutils import ( "context" @@ -11,69 +11,69 @@ const ( contextKeyRequestID contextKey = "REQUEST_ID" ) -// APIServerContext is a context.Context wrapper that +// ServerContext is a context.Context wrapper that // enables custom logs with request ID. -type APIServerContext struct { +type ServerContext struct { context.Context } -// ToAPIServerContext takes a context.Context instance -// and converts it to *ApiServerContext. -func ToAPIServerContext(ctx context.Context) *APIServerContext { - if asCtx, ok := ctx.(*APIServerContext); ok { +// ToServerContext takes a context.Context instance +// and converts it to *ServerContext. +func ToServerContext(ctx context.Context) *ServerContext { + if asCtx, ok := ctx.(*ServerContext); ok { return asCtx } - return &APIServerContext{Context: ctx} + return &ServerContext{Context: ctx} } // SetRequestID associates a request ID for the context. -func (ctx *APIServerContext) SetRequestID(requestID uint64) context.Context { +func (ctx *ServerContext) SetRequestID(requestID uint64) context.Context { context.WithValue(ctx, contextKeyRequestID, requestID) return ctx } -func (ctx *APIServerContext) requestID() uint64 { +func (ctx *ServerContext) requestID() uint64 { id := ctx.Value(contextKeyRequestID) uint64ID, _ := id.(uint64) return uint64ID } -func (ctx *APIServerContext) getLogString(format string, params ...interface{}) string { +func (ctx *ServerContext) getLogString(format string, params ...interface{}) string { return fmt.Sprintf("RID %d: ", ctx.requestID()) + fmt.Sprintf(format, params...) } // Tracef writes a customized formatted context // related log with log level 'Trace'. -func (ctx *APIServerContext) Tracef(format string, params ...interface{}) { +func (ctx *ServerContext) Tracef(format string, params ...interface{}) { log.Trace(ctx.getLogString(format, params...)) } // Debugf writes a customized formatted context // related log with log level 'Debug'. -func (ctx *APIServerContext) Debugf(format string, params ...interface{}) { +func (ctx *ServerContext) Debugf(format string, params ...interface{}) { log.Debug(ctx.getLogString(format, params...)) } // Infof writes a customized formatted context // related log with log level 'Info'. -func (ctx *APIServerContext) Infof(format string, params ...interface{}) { +func (ctx *ServerContext) Infof(format string, params ...interface{}) { log.Info(ctx.getLogString(format, params...)) } // Warnf writes a customized formatted context // related log with log level 'Warn'. -func (ctx *APIServerContext) Warnf(format string, params ...interface{}) { +func (ctx *ServerContext) Warnf(format string, params ...interface{}) { log.Warn(ctx.getLogString(format, params...)) } // Errorf writes a customized formatted context // related log with log level 'Error'. -func (ctx *APIServerContext) Errorf(format string, params ...interface{}) { +func (ctx *ServerContext) Errorf(format string, params ...interface{}) { log.Error(ctx.getLogString(format, params...)) } // Criticalf writes a customized formatted context // related log with log level 'Critical'. -func (ctx *APIServerContext) Criticalf(format string, params ...interface{}) { +func (ctx *ServerContext) Criticalf(format string, params ...interface{}) { log.Criticalf(ctx.getLogString(format, params...)) } diff --git a/apiserver/utils/error.go b/httpserverutils/error.go similarity index 71% rename from apiserver/utils/error.go rename to httpserverutils/error.go index 198b7f6b6..5a1c6786a 100644 --- a/apiserver/utils/error.go +++ b/httpserverutils/error.go @@ -1,6 +1,7 @@ -package utils +package httpserverutils import ( + "encoding/json" "fmt" "github.com/jinzhu/gorm" "net/http" @@ -72,3 +73,39 @@ func IsDBRecordNotFoundError(dbErrors []error) bool { func HasDBError(dbErrors []error) bool { return !IsDBRecordNotFoundError(dbErrors) && len(dbErrors) > 0 } + +// ClientError is the http response that is sent to the +// client in case of an error. +type ClientError struct { + ErrorCode int `json:"errorCode"` + ErrorMessage string `json:"errorMessage"` +} + +func (err *ClientError) Error() string { + return fmt.Sprintf("%s (Code: %d)", err.ErrorMessage, err.ErrorCode) +} + +// SendErr takes a HandlerError and create a ClientError out of it that is sent +// to the http client. +func SendErr(ctx *ServerContext, w http.ResponseWriter, hErr *HandlerError) { + errMsg := fmt.Sprintf("got error: %s", hErr) + ctx.Warnf(errMsg) + w.WriteHeader(hErr.Code) + SendJSONResponse(w, &ClientError{ + ErrorCode: hErr.Code, + ErrorMessage: hErr.ClientMessage, + }) +} + +// SendJSONResponse encodes the given response to JSON format and +// sends it to the client +func SendJSONResponse(w http.ResponseWriter, response interface{}) { + b, err := json.Marshal(response) + if err != nil { + panic(err) + } + _, err = fmt.Fprintf(w, string(b)) + if err != nil { + panic(err) + } +} diff --git a/apiserver/utils/log.go b/httpserverutils/log.go similarity index 89% rename from apiserver/utils/log.go rename to httpserverutils/log.go index e2d93e8ac..402395134 100644 --- a/apiserver/utils/log.go +++ b/httpserverutils/log.go @@ -1,4 +1,4 @@ -package utils +package httpserverutils import "github.com/daglabs/btcd/util/panics" import "github.com/daglabs/btcd/apiserver/logger" diff --git a/apiserver/server/middlewares.go b/httpserverutils/middlewares.go similarity index 50% rename from apiserver/server/middlewares.go rename to httpserverutils/middlewares.go index 04ceae289..87b2974dd 100644 --- a/apiserver/server/middlewares.go +++ b/httpserverutils/middlewares.go @@ -1,48 +1,56 @@ -package server +package httpserverutils import ( "fmt" - "github.com/daglabs/btcd/apiserver/utils" "net/http" "runtime/debug" ) var nextRequestID uint64 = 1 -func addRequestMetadataMiddleware(next http.Handler) http.Handler { +// AddRequestMetadataMiddleware is a middleware that adds some +// metadata to the context of every request. +func AddRequestMetadataMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - rCtx := utils.ToAPIServerContext(r.Context()).SetRequestID(nextRequestID) + rCtx := ToServerContext(r.Context()).SetRequestID(nextRequestID) r.WithContext(rCtx) nextRequestID++ next.ServeHTTP(w, r) }) } -func loggingMiddleware(next http.Handler) http.Handler { +// LoggingMiddleware is a middleware that writes +// logs for every request. +func LoggingMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - ctx := utils.ToAPIServerContext(r.Context()) + ctx := ToServerContext(r.Context()) ctx.Infof("Method: %s URI: %s", r.Method, r.RequestURI) next.ServeHTTP(w, r) }) } -func recoveryMiddleware(h http.Handler) http.Handler { +// RecoveryMiddleware is a middleware that recovers +// from panics, log it, and sends Internal Server +// Error to the client. +func RecoveryMiddleware(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - ctx := utils.ToAPIServerContext(r.Context()) + ctx := ToServerContext(r.Context()) defer func() { recoveryErr := recover() if recoveryErr != nil { recoveryErrStr := fmt.Sprintf("%s", recoveryErr) log.Criticalf("Fatal error: %s", recoveryErrStr) log.Criticalf("Stack trace: %s", debug.Stack()) - sendErr(ctx, w, utils.NewInternalServerHandlerError(recoveryErrStr)) + SendErr(ctx, w, NewInternalServerHandlerError(recoveryErrStr)) } }() h.ServeHTTP(w, r) }) } -func setJSONMiddleware(h http.Handler) http.Handler { +// SetJSONMiddleware is a middleware that sets the content type of +// every request to be application/json. +func SetJSONMiddleware(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=utf-8") h.ServeHTTP(w, r) diff --git a/httpserverutils/request.go b/httpserverutils/request.go new file mode 100644 index 000000000..8a4920fc4 --- /dev/null +++ b/httpserverutils/request.go @@ -0,0 +1,58 @@ +package httpserverutils + +import ( + "fmt" + "github.com/gorilla/mux" + "io/ioutil" + "net/http" +) + +// HandlerFunc is a handler function that is passed to the +// MakeHandler wrapper and gets the relevant request fields +// from it. +type HandlerFunc func(ctx *ServerContext, r *http.Request, routeParams map[string]string, queryParams map[string]string, requestBody []byte) ( + interface{}, *HandlerError) + +// MakeHandler is a wrapper function that takes a handler in the form of HandlerFunc +// and returns a function that can be used as a handler in mux.Router.HandleFunc. +func MakeHandler(handler HandlerFunc) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + ctx := ToServerContext(r.Context()) + + var requestBody []byte + if r.Method == "POST" { + var err error + requestBody, err = ioutil.ReadAll(r.Body) + if err != nil { + SendErr(ctx, w, NewInternalServerHandlerError("Error reading POST data")) + } + } + + flattenedQueryParams, hErr := flattenQueryParams(r.URL.Query()) + if hErr != nil { + SendErr(ctx, w, hErr) + return + } + + response, hErr := handler(ctx, r, mux.Vars(r), flattenedQueryParams, requestBody) + if hErr != nil { + SendErr(ctx, w, hErr) + return + } + if response != nil { + SendJSONResponse(w, response) + } + } +} + +func flattenQueryParams(queryParams map[string][]string) (map[string]string, *HandlerError) { + flattenedMap := make(map[string]string) + for param, valuesSlice := range queryParams { + if len(valuesSlice) > 1 { + return nil, NewHandlerError(http.StatusUnprocessableEntity, fmt.Sprintf("Couldn't parse the '%s' query parameter:"+ + " expected a single value but got multiple values", param)) + } + flattenedMap[param] = valuesSlice[0] + } + return flattenedMap, nil +} diff --git a/server/rpc/handle_get_blocks.go b/server/rpc/handle_get_blocks.go index 14fa156a4..19eee3f45 100644 --- a/server/rpc/handle_get_blocks.go +++ b/server/rpc/handle_get_blocks.go @@ -10,7 +10,7 @@ import ( const ( // maxBlocksInGetBlocksResult is the max amount of blocks that are // allowed in a GetBlocksResult. - maxBlocksInGetBlocksResult = 1000 + maxBlocksInGetBlocksResult = 100 ) func handleGetBlocks(s *Server, cmd interface{}, closeChan <-chan struct{}) (interface{}, error) { diff --git a/util/panics/panics.go b/util/panics/panics.go index 0e65c4503..1a09a5c4d 100644 --- a/util/panics/panics.go +++ b/util/panics/panics.go @@ -9,7 +9,7 @@ import ( // HandlePanic recovers panics, log them, and then exits the process. func HandlePanic(log logs.Logger, backendLog *logs.Backend) { if err := recover(); err != nil { - log.Criticalf("Fatal error: %s", err) + log.Criticalf("Fatal error: %+v", err) log.Criticalf("Stack trace: %s", debug.Stack()) if backendLog != nil { backendLog.Close()