Skip to content

Commit a19427f

Browse files
committed
[chronik] add blockchain.transaction.broadcast electrum method
Summary: A rpc to broadcast a raw hex transaction. No checks of any kind are done before broadcasting (tx may burn funds or tokens). Test Plan: `ninja check-functional` Reviewers: #bitcoin_abc, tobias_ruck, Fabien Reviewed By: #bitcoin_abc, tobias_ruck, Fabien Subscribers: tobias_ruck, Fabien Differential Revision: https://reviews.bitcoinabc.org/D17370
1 parent 77288ad commit a19427f

File tree

4 files changed

+243
-3
lines changed

4 files changed

+243
-3
lines changed

Cargo.lock

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

chronik/chronik-http/Cargo.toml

+3
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ async-trait = "0.1"
3131
# HTTP webapps
3232
axum = { version = "0.7", features = ["ws"] }
3333

34+
# Efficient byte strings
35+
bytes = "1.4"
36+
3437
# Async toolkit
3538
futures = "0.3"
3639

chronik/chronik-http/src/electrum.rs

+34
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ use bitcoinsuite_core::{
1111
hash::{Hashed, Sha256, Sha256d},
1212
tx::TxId,
1313
};
14+
use bytes::Bytes;
15+
use chronik_bridge::ffi;
1416
use chronik_indexer::merkle::MerkleTree;
1517
use futures::future;
1618
use itertools::izip;
@@ -494,6 +496,38 @@ impl ChronikElectrumRPCBlockchainEndpoint {
494496
}
495497
}
496498

499+
#[rpc_method(name = "transaction.broadcast")]
500+
async fn transaction_broadcast(
501+
&self,
502+
params: Value,
503+
) -> Result<Value, RPCError> {
504+
check_max_number_of_params!(params, 1);
505+
let raw_tx = match get_param!(params, 0, "raw_tx")? {
506+
Value::String(raw_tx) => Ok(raw_tx),
507+
_ => Err(RPCError::CustomError(
508+
1,
509+
"Invalid raw_tx argument; expected hex string".to_string(),
510+
)),
511+
}?;
512+
let raw_tx = Bytes::from(hex::decode(raw_tx).map_err(|_err| {
513+
RPCError::CustomError(
514+
1,
515+
"Failed to decode raw_tx as a hex string".to_string(),
516+
)
517+
})?);
518+
519+
let max_fee = ffi::calc_fee(
520+
raw_tx.len(),
521+
ffi::default_max_raw_tx_fee_rate_per_kb(),
522+
);
523+
let txid = match self.node.bridge.broadcast_tx(&raw_tx, max_fee) {
524+
Ok(txid) => Ok(TxId::from(txid)),
525+
Err(err) => Err(RPCError::CustomError(1, err.what().to_string())),
526+
}?;
527+
528+
Ok(Value::String(txid.to_string()))
529+
}
530+
497531
#[rpc_method(name = "transaction.get")]
498532
async fn transaction_get(&self, params: Value) -> Result<Value, RPCError> {
499533
check_max_number_of_params!(params, 2);

test/functional/chronik_electrum_blockchain.py

+205-3
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,17 @@
1212
TIME_GENESIS_BLOCK,
1313
)
1414
from test_framework.merkle import merkle_root_and_branch
15+
from test_framework.messages import (
16+
COutPoint,
17+
CTransaction,
18+
CTxIn,
19+
CTxOut,
20+
FromHex,
21+
ToHex,
22+
)
23+
from test_framework.script import OP_RETURN, OP_TRUE, CScript
1524
from test_framework.test_framework import BitcoinTestFramework
25+
from test_framework.txtools import pad_tx
1626
from test_framework.util import assert_equal, hex_to_be_bytes
1727
from test_framework.wallet import MiniWallet
1828

@@ -44,6 +54,7 @@ def run_test(self):
4454
self.test_invalid_params()
4555
self.test_transaction_get()
4656
self.test_transaction_get_height()
57+
self.test_transaction_broadcast()
4758
self.test_transaction_get_merkle()
4859
self.test_block_header()
4960

@@ -158,7 +169,7 @@ def test_transaction_get(self):
158169
},
159170
)
160171

161-
self.generate(self.nodes[0], 2)
172+
self.generate(self.wallet, 2)
162173
assert_equal(
163174
self.client.blockchain.transaction.get(
164175
txid=GENESIS_CB_TXID, verbose=True
@@ -171,7 +182,11 @@ def test_transaction_get_height(self):
171182
assert_equal(response.result, 0)
172183

173184
self.wallet.rescan_utxos()
174-
tx = self.wallet.send_self_transfer(from_node=self.node)
185+
tx = self.wallet.create_self_transfer()
186+
187+
response = self.client.blockchain.transaction.broadcast(tx["hex"])
188+
assert_equal(response.result, tx["txid"])
189+
self.node.syncwithvalidationinterfacequeue()
175190

176191
response = self.client.blockchain.transaction.get(tx["txid"])
177192
assert_equal(response.result, tx["hex"])
@@ -181,13 +196,200 @@ def test_transaction_get_height(self):
181196
assert_equal(response.result, 0)
182197

183198
# Mine the tx
184-
self.generate(self.node, 1)
199+
self.generate(self.wallet, 1)
185200
response = self.client.blockchain.transaction.get_height(tx["txid"])
186201
assert_equal(response.result, 203)
187202

188203
response = self.client.blockchain.transaction.get_height(32 * "ff")
189204
assert_equal(response.error, {"code": -32600, "message": "Unknown txid"})
190205

206+
def test_transaction_broadcast(self):
207+
tx = self.wallet.create_self_transfer()
208+
209+
for _ in range(3):
210+
response = self.client.blockchain.transaction.broadcast(tx["hex"])
211+
assert_equal(response.result, tx["txid"])
212+
213+
self.generate(self.wallet, 1)
214+
response = self.client.blockchain.transaction.broadcast(tx["hex"])
215+
assert_equal(
216+
response.error, {"code": 1, "message": "Transaction already in block chain"}
217+
)
218+
219+
spent_utxo = tx["tx"].vin[0]
220+
221+
tx_obj = self.wallet.create_self_transfer()["tx"]
222+
tx_obj.vin[0] = spent_utxo
223+
response = self.client.blockchain.transaction.broadcast(ToHex(tx_obj))
224+
assert_equal(
225+
response.error,
226+
{"code": 1, "message": "Missing inputs: bad-txns-inputs-missingorspent"},
227+
)
228+
229+
raw_tx_reference = self.wallet.create_self_transfer()["hex"]
230+
231+
tx_obj = FromHex(CTransaction(), raw_tx_reference)
232+
tx_obj.vin[0].scriptSig = b"aaaaaaaaaaaaaaa"
233+
response = self.client.blockchain.transaction.broadcast(ToHex(tx_obj))
234+
assert_equal(
235+
response.error,
236+
{
237+
"code": 1,
238+
"message": "Transaction rejected by mempool: scriptsig-not-pushonly",
239+
},
240+
)
241+
242+
tx_obj = FromHex(CTransaction(), raw_tx_reference)
243+
tx_obj.vout[0].scriptPubKey = CScript([OP_RETURN, b"\xff"])
244+
tx_obj.vout = [tx_obj.vout[0]] * 2
245+
response = self.client.blockchain.transaction.broadcast(ToHex(tx_obj))
246+
assert_equal(
247+
response.error,
248+
{"code": 1, "message": "Transaction rejected by mempool: multi-op-return"},
249+
)
250+
251+
tx_obj = FromHex(CTransaction(), raw_tx_reference)
252+
tx_obj.vin[0].nSequence = 0xFFFFFFFE
253+
tx_obj.nLockTime = self.node.getblockcount() + 1
254+
response = self.client.blockchain.transaction.broadcast(ToHex(tx_obj))
255+
assert_equal(
256+
response.error,
257+
{
258+
"code": 1,
259+
"message": "Transaction rejected by mempool: bad-txns-nonfinal, non-final transaction",
260+
},
261+
)
262+
263+
tx_obj = FromHex(CTransaction(), raw_tx_reference)
264+
tx_obj.vout = []
265+
response = self.client.blockchain.transaction.broadcast(ToHex(tx_obj))
266+
assert_equal(
267+
response.error,
268+
{
269+
"code": 1,
270+
"message": "Transaction rejected by mempool: bad-txns-vout-empty",
271+
},
272+
)
273+
274+
# Non-standard script
275+
tx_obj.vout.append(CTxOut(0, CScript([OP_TRUE])))
276+
response = self.client.blockchain.transaction.broadcast(ToHex(tx_obj))
277+
assert_equal(
278+
response.error,
279+
{"code": 1, "message": "Transaction rejected by mempool: scriptpubkey"},
280+
)
281+
282+
tx_obj.vout[0] = CTxOut(0, CScript([OP_RETURN, b"\xff"]))
283+
assert len(ToHex(tx_obj)) // 2 < 100
284+
response = self.client.blockchain.transaction.broadcast(ToHex(tx_obj))
285+
assert_equal(
286+
response.error,
287+
{
288+
"code": 1,
289+
"message": "Transaction rejected by mempool: bad-txns-undersize",
290+
},
291+
)
292+
293+
tx_obj = self.wallet.create_self_transfer()["tx"]
294+
pad_tx(tx_obj, 100_001)
295+
response = self.client.blockchain.transaction.broadcast(ToHex(tx_obj))
296+
assert_equal(
297+
response.error,
298+
{"code": 1, "message": "Transaction rejected by mempool: tx-size"},
299+
)
300+
301+
tx_obj = FromHex(CTransaction(), raw_tx_reference)
302+
tx_obj.vin.append(tx_obj.vin[0])
303+
response = self.client.blockchain.transaction.broadcast(ToHex(tx_obj))
304+
assert_equal(
305+
response.error,
306+
{
307+
"code": 1,
308+
"message": "Transaction rejected by mempool: bad-txns-inputs-duplicate",
309+
},
310+
)
311+
312+
tx_obj.vin = []
313+
response = self.client.blockchain.transaction.broadcast(ToHex(tx_obj))
314+
assert_equal(
315+
response.error,
316+
{
317+
"code": 1,
318+
"message": "Transaction rejected by mempool: bad-txns-vin-empty",
319+
},
320+
)
321+
322+
tx_obj = FromHex(CTransaction(), raw_tx_reference)
323+
tx_obj.nVersion = 1337
324+
response = self.client.blockchain.transaction.broadcast(ToHex(tx_obj))
325+
assert_equal(
326+
response.error,
327+
{"code": 1, "message": "Transaction rejected by mempool: version"},
328+
)
329+
330+
# Coinbase input in first position
331+
tx_obj = FromHex(CTransaction(), raw_tx_reference)
332+
tx_obj.vin[0] = CTxIn(COutPoint(txid=0, n=0xFFFFFFFF))
333+
response = self.client.blockchain.transaction.broadcast(ToHex(tx_obj))
334+
assert_equal(
335+
response.error,
336+
{"code": 1, "message": "Transaction rejected by mempool: bad-tx-coinbase"},
337+
)
338+
339+
# Coinbase input in second position
340+
tx_obj = FromHex(CTransaction(), raw_tx_reference)
341+
tx_obj.vin.append(CTxIn(COutPoint(txid=0, n=0xFFFFFFFF)))
342+
response = self.client.blockchain.transaction.broadcast(ToHex(tx_obj))
343+
assert_equal(
344+
response.error,
345+
{
346+
"code": 1,
347+
"message": "Transaction rejected by mempool: bad-txns-prevout-null",
348+
},
349+
)
350+
351+
tx = self.wallet.create_self_transfer(fee_rate=0, fee=0)
352+
response = self.client.blockchain.transaction.broadcast(tx["hex"])
353+
assert_equal(
354+
response.error,
355+
{
356+
"code": 1,
357+
"message": "Transaction rejected by mempool: min relay fee not met, 0 < 100",
358+
},
359+
)
360+
361+
tx = self.wallet.create_self_transfer(fee_rate=10_000_000, fee=0)
362+
response = self.client.blockchain.transaction.broadcast(tx["hex"])
363+
assert_equal(
364+
response.error,
365+
{
366+
"code": 1,
367+
"message": "Fee exceeds maximum configured by user (e.g. -maxtxfee, maxfeerate)",
368+
},
369+
)
370+
371+
# Mine enough blocks to ensure that the following test does not try to spend
372+
# a utxo already spent in a previous test.
373+
# Invalidate two blocks, so that miniwallet has access to a coin that
374+
# will mature in the next block.
375+
self.generate(self.wallet, 100)
376+
chain_height = self.node.getblockcount() - 3
377+
block_to_invalidate = self.node.getblockhash(chain_height + 1)
378+
self.node.invalidateblock(block_to_invalidate)
379+
immature_txid = self.nodes[0].getblock(
380+
self.nodes[0].getblockhash(chain_height - 100 + 2)
381+
)["tx"][0]
382+
immature_utxo = self.wallet.get_utxo(txid=immature_txid)
383+
tx = self.wallet.create_self_transfer(utxo_to_spend=immature_utxo)
384+
response = self.client.blockchain.transaction.broadcast(tx["hex"])
385+
assert_equal(
386+
response.error,
387+
{
388+
"code": 1,
389+
"message": "Transaction rejected by mempool: bad-txns-premature-spend-of-coinbase, tried to spend coinbase at depth 99",
390+
},
391+
)
392+
191393
def test_transaction_get_merkle(self):
192394
for _ in range(42):
193395
self.wallet.send_self_transfer(from_node=self.node)

0 commit comments

Comments
 (0)