Skip to content

Commit

Permalink
rest: Add "/broadcast" endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
danielabrozzoni committed Oct 10, 2024
1 parent caf44e5 commit aa3209b
Show file tree
Hide file tree
Showing 4 changed files with 65 additions and 4 deletions.
7 changes: 6 additions & 1 deletion doc/REST-interface.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<BLOCK-HASH>.<bin|hex|json>`
- `GET /rest/block/notxdetails/<BLOCK-HASH>.<bin|hex|json>`
Expand Down Expand Up @@ -143,7 +149,6 @@ Refer to the `getrawmempool` RPC help for details. Defaults to setting

*Query parameters for `verbose` and `mempool_sequence` available in 25.0 and up.*


Risks
-------------
Running a web browser on the same node with a REST enabled bitcoind can be a risk. Accessing prepared XSS websites could read out tx/block data of your node by placing links like `<script src="http://127.0.0.1:8332/rest/tx/1234567890.json">` which might break the nodes privacy.
6 changes: 3 additions & 3 deletions src/node/transaction.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 server and wallet are started
// and reset after the RPC/REST sever and wallet are stopped.
assert(node.chainman);
assert(node.mempool);
assert(node.peerman);
Expand Down
44 changes: 44 additions & 0 deletions src/rest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.

#include "node/transaction.h"
#include <config/bitcoin-config.h> // IWYU pragma: keep

#include <rest.h>

#include "logging.h"
#include "node/types.h"
#include <blockfilter.h>
#include <chain.h>
#include <chainparams.h>
Expand Down Expand Up @@ -1006,6 +1009,46 @@ 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;
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");
}

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) {
req->WriteHeader("Content-Type", "text/plain");
req->WriteReply(HTTP_BAD_REQUEST, "Error while broadcasting: " + err_string);
}

switch (rf) {
case RESTResponseFormat::HEX: {
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);
Expand All @@ -1022,6 +1065,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)
Expand Down
12 changes: 12 additions & 0 deletions test/functional/interface_rest.py
Original file line number Diff line number Diff line change
Expand Up @@ -440,5 +440,17 @@ 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[0].getrawmempool()

# Check invalid requests
self.test_rest_request("/broadcast/123", http_method='POST', req_type=ReqType.HEX, body=tx["hex"], status=400, ret_type=RetType.OBJ)
self.test_rest_request("/broadcast", http_method="POST", req_type=ReqType.HEX, body="0000", status=400, ret_type=RetType.OBJ)
self.test_rest_request("/broadcast", http_method="GET", req_type=ReqType.HEX, status=400, ret_type=RetType.OBJ)

if __name__ == '__main__':
RESTTest(__file__).main()

0 comments on commit aa3209b

Please sign in to comment.