From 6b32a886e351f0af3084ccab076a52e0eafc5aab Mon Sep 17 00:00:00 2001 From: Daniela Brozzoni Date: Tue, 8 Oct 2024 15:28:29 +0200 Subject: [PATCH] rest: Add "/broadcast" endpoint This commit adds a REST endpoint to allow broadcasting a transaction: `POST /rest/broadcast.hex` The transaction hex must be passed in the body of the request; on success, the txid of the broadcasted transaction will be returned. Fixes #31017 --- doc/REST-interface.md | 6 +++++ doc/release-notes-31065.md | 4 +++ src/node/transaction.cpp | 6 ++--- src/rest.cpp | 41 +++++++++++++++++++++++++++++++ test/functional/interface_rest.py | 15 +++++++++++ 5 files changed, 69 insertions(+), 3 deletions(-) create mode 100644 doc/release-notes-31065.md diff --git a/doc/REST-interface.md b/doc/REST-interface.md index 6664bc2a3ae1d..945a4441db5c3 100644 --- a/doc/REST-interface.md +++ b/doc/REST-interface.md @@ -36,6 +36,12 @@ Responds with 404 if the transaction doesn't exist. By default, this endpoint will only search the mempool. To query for a confirmed transaction, enable the transaction index via "txindex=1" command line / configuration option. +`POST /rest/broadcast.hex` + +Broadcasts a transaction. +The transaction hex must be passed in the body of the request. +Returns the txid if the transaction was broadcasted correctly, responds with 400 if the transaction hex can't be parsed or if broadcasting failed. + #### Blocks - `GET /rest/block/.` - `GET /rest/block/notxdetails/.` diff --git a/doc/release-notes-31065.md b/doc/release-notes-31065.md new file mode 100644 index 0000000000000..453ced5eaf3c0 --- /dev/null +++ b/doc/release-notes-31065.md @@ -0,0 +1,4 @@ +Updated REST APIs +----------------------- + +- A new `/rest/broadcast/` endpoint has been added to allow broadcasting a transaction from the REST interface. (#31065) diff --git a/src/node/transaction.cpp b/src/node/transaction.cpp index 0f45da45dbdbb..6e9ad91a6802a 100644 --- a/src/node/transaction.cpp +++ b/src/node/transaction.cpp @@ -33,9 +33,9 @@ static TransactionError HandleATMPError(const TxValidationState& state, std::str TransactionError BroadcastTransaction(NodeContext& node, const CTransactionRef tx, std::string& err_string, const CAmount& max_tx_fee, bool relay, bool wait_callback) { - // BroadcastTransaction can be called by RPC or by the wallet. - // chainman, mempool and peerman are initialized before the RPC server and wallet are started - // and reset after the RPC sever and wallet are stopped. + // BroadcastTransaction can be called by RPC, REST, or by the wallet. + // chainman, mempool and peerman are initialized before the RPC/REST servers and wallet are started + // and reset after the RPC/REST servers and wallet are stopped. assert(node.chainman); assert(node.mempool); assert(node.peerman); diff --git a/src/rest.cpp b/src/rest.cpp index 4732922a156da..dc1f8b0c31716 100644 --- a/src/rest.cpp +++ b/src/rest.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -1006,6 +1007,45 @@ static bool rest_blockhash_by_height(const std::any& context, HTTPRequest* req, } } +static bool rest_broadcast(const std::any& context, HTTPRequest* req, const std::string& str_uri_part) +{ + if (!CheckWarmup(req)) + return false; + const std::string body = req->ReadBody(); + std::string params; + const RESTResponseFormat rf = ParseDataFormat(params, str_uri_part); + if (params != "") { + return RESTERR(req, HTTP_BAD_REQUEST, "Invalid URI format. Expected /rest/broadcast.hex"); + } + + CMutableTransaction mtx; + if (!DecodeHexTx(mtx, body)) { + return RESTERR(req, HTTP_BAD_REQUEST, "TX decode failed"); + } + + switch (rf) { + case RESTResponseFormat::HEX: { + const CTransactionRef tx(MakeTransactionRef(std::move(mtx))); + std::string err_string; + NodeContext* node = GetNodeContext(context, req); + if (!node) return false; + + const node::TransactionError error = node::BroadcastTransaction(*node, tx, err_string, /*max_tx_fee=*/0, /*relay=*/true, /*wait_callback=*/true); + if (node::TransactionError::OK != error) { + return RESTERR(req, HTTP_BAD_REQUEST, "Error while broadcasting: " + err_string); + } + + req->WriteHeader("Content-Type", "text/plain"); + req->WriteReply(HTTP_OK, tx->GetHash().GetHex() + "\n"); + return true; + } + + default: { + return RESTERR(req, HTTP_NOT_FOUND, "output format not found (available: hex)"); + } + } +} + static const struct { const char* prefix; bool (*handler)(const std::any& context, HTTPRequest* req, const std::string& strReq); @@ -1022,6 +1062,7 @@ static const struct { {"/rest/deploymentinfo/", rest_deploymentinfo}, {"/rest/deploymentinfo", rest_deploymentinfo}, {"/rest/blockhashbyheight/", rest_blockhash_by_height}, + {"/rest/broadcast", rest_broadcast}, }; void StartREST(const std::any& context) diff --git a/test/functional/interface_rest.py b/test/functional/interface_rest.py index ba6e960476b98..162707494fabe 100755 --- a/test/functional/interface_rest.py +++ b/test/functional/interface_rest.py @@ -440,5 +440,20 @@ def run_test(self): resp = self.test_rest_request(f"/deploymentinfo/{INVALID_PARAM}", ret_type=RetType.OBJ, status=400) assert_equal(resp.read().decode('utf-8').rstrip(), f"Invalid hash: {INVALID_PARAM}") + self.log.info("Test the /broadcast URI") + tx = self.wallet.create_self_transfer() + resp_hex = self.test_rest_request("/broadcast", http_method='POST', req_type=ReqType.HEX, body=tx["hex"], ret_type=RetType.OBJ) + assert_equal(resp_hex.read().decode('utf-8').rstrip(), tx["txid"]) + self.sync_all() + assert tx["txid"] in self.nodes[1].getrawmempool() + + # Check invalid requests + resp = self.test_rest_request("/broadcast/123", http_method='POST', req_type=ReqType.HEX, body=tx["hex"], status=400, ret_type=RetType.OBJ) + assert_equal(resp.read().decode('utf-8').rstrip(), f"Invalid URI format. Expected /rest/broadcast.hex") + resp = self.test_rest_request("/broadcast", http_method="POST", req_type=ReqType.HEX, body="0000", status=400, ret_type=RetType.OBJ) + assert_equal(resp.read().decode('utf-8').rstrip(), f"TX decode failed") + resp = self.test_rest_request("/broadcast", http_method="POST", req_type=ReqType.JSON, body=tx["hex"], status=404, ret_type=RetType.OBJ) + assert_equal(resp.read().decode('utf-8').rstrip(), f"output format not found (available: hex)") + if __name__ == '__main__': RESTTest(__file__).main()