diff --git a/include/bitcoin/server/interfaces/electrum.hpp b/include/bitcoin/server/interfaces/electrum.hpp index ae180f90..7aba73da 100644 --- a/include/bitcoin/server/interfaces/electrum.hpp +++ b/include/bitcoin/server/interfaces/electrum.hpp @@ -46,7 +46,7 @@ struct electrum_methods method<"blockchain.transaction.broadcast", string_t>{ "raw_tx" }, method<"blockchain.transaction.get", string_t, boolean_t>{ "tx_hash", "verbose" }, method<"blockchain.transaction.get_merkle", string_t, number_t>{ "tx_hash", "height" }, - method<"blockchain.transaction.id_from_pos", number_t, number_t, boolean_t>{ "height", "tx_pos", "merkle" }, + method<"blockchain.transaction.id_from_pos", number_t, number_t, optional>{ "height", "tx_pos", "merkle" }, /// Server methods. method<"server.add_peer", object_t>{ "features" }, diff --git a/src/protocols/protocol_electrum.cpp b/src/protocols/protocol_electrum.cpp index bd95faec..976bc4ba 100644 --- a/src/protocols/protocol_electrum.cpp +++ b/src/protocols/protocol_electrum.cpp @@ -270,7 +270,7 @@ void protocol_electrum::blockchain_block_headers(size_t starting, array_t branch(proof.size()); std::ranges::transform(proof, branch.begin(), - [](const auto& hash) { return encode_hash(hash); }); + [](const auto& hash) NOEXCEPT { return encode_hash(hash); }); result["branch"] = std::move(branch); result["root"] = encode_hash(root); @@ -526,11 +526,73 @@ void protocol_electrum::handle_blockchain_transaction_get_merkle(const code& ec, } void protocol_electrum::handle_blockchain_transaction_id_from_pos(const code& ec, - rpc_interface::blockchain_transaction_id_from_pos, double , - double , bool ) NOEXCEPT + rpc_interface::blockchain_transaction_id_from_pos, double height, + double tx_pos, bool merkle) NOEXCEPT { - if (stopped(ec)) return; - send_code(error::not_implemented); + if (stopped(ec)) + return; + + size_t position{}; + size_t block_height{}; + if (!to_integer(block_height, height) || + !to_integer(position, tx_pos)) + { + send_code(error::invalid_argument); + return; + } + + const auto& query = archive(); + const auto block_link = query.to_confirmed(block_height); + const auto tx_link = query.get_position_tx(block_link, position); + if (tx_link.is_terminal()) + { + send_code(error::not_found); + return; + } + + using namespace system; + const auto hash = query.get_tx_key(tx_link); + if (hash == null_hash) + { + send_code(error::server_error); + return; + } + + if (!merkle) + { + send_result(encode_hash(hash), two * hash_size, BIND(complete, _1)); + } + else + { + auto hashes = query.get_tx_keys(block_link); + if (hashes.empty()) + { + send_code(error::server_error); + return; + } + + if (position >= hashes.size()) + { + send_code(error::not_found); + return; + } + + using namespace chain; + const auto proof = block::merkle_branch(position, std::move(hashes)); + + array_t branch(proof.size()); + std::ranges::transform(proof, branch.begin(), + [](const auto& hash) NOEXCEPT { return encode_hash(hash); }); + + send_result( + { + object_t + { + { "tx_hash", encode_hash(hash) }, + { "merkle", std::move(branch) } + } + }, two * hash_size * add1(branch.size()), BIND(complete, _1)); + } } // Handlers (server). diff --git a/test/protocols/electrum/electrum_transactions.cpp b/test/protocols/electrum/electrum_transactions.cpp index a87f61cc..42c1f02c 100644 --- a/test/protocols/electrum/electrum_transactions.cpp +++ b/test/protocols/electrum/electrum_transactions.cpp @@ -98,9 +98,9 @@ BOOST_AUTO_TEST_CASE(electrum__blockchain_transaction_get__missing_param__droppe BOOST_CHECK(handshake()); const auto& coinbase = *genesis.transactions_ptr()->front(); - const auto tx_hash = encode_hash(coinbase.hash(false)); + const auto tx0_hash = encode_hash(coinbase.hash(false)); const auto request = R"({"id":80,"method":"blockchain.transaction.get","params":["%1%"]})" "\n"; - const auto response = get((boost::format(request) % tx_hash).str()); + const auto response = get((boost::format(request) % tx0_hash).str()); BOOST_CHECK(response.at("dropped").as_bool()); } @@ -109,9 +109,9 @@ BOOST_AUTO_TEST_CASE(electrum__blockchain_transaction_get__extra_param__dropped) BOOST_CHECK(handshake()); const auto& coinbase = *genesis.transactions_ptr()->front(); - const auto tx_hash = encode_hash(coinbase.hash(false)); + const auto tx0_hash = encode_hash(coinbase.hash(false)); const auto request = R"({"id":81,"method":"blockchain.transaction.get","params":["%1%",false,"extra"]})" "\n"; - const auto response = get((boost::format(request) % tx_hash).str()); + const auto response = get((boost::format(request) % tx0_hash).str()); BOOST_CHECK(response.at("dropped").as_bool()); } @@ -120,9 +120,9 @@ BOOST_AUTO_TEST_CASE(electrum__blockchain_transaction_get__genesis_coinbase_verb BOOST_CHECK(handshake()); const auto& coinbase = *genesis.transactions_ptr()->front(); - const auto tx_hash = encode_hash(coinbase.hash(false)); + const auto tx0_hash = encode_hash(coinbase.hash(false)); const auto request = R"({"id":82,"method":"blockchain.transaction.get","params":["%1%",false]})" "\n"; - const auto response = get((boost::format(request) % tx_hash).str()); + const auto response = get((boost::format(request) % tx0_hash).str()); BOOST_CHECK_EQUAL(response.at("result").as_string(), encode_base16(coinbase.to_data(true))); } @@ -131,9 +131,9 @@ BOOST_AUTO_TEST_CASE(electrum__blockchain_transaction_get__genesis_coinbase_verb BOOST_CHECK(handshake()); const auto& coinbase = *genesis.transactions_ptr()->front(); - const auto tx_hash = encode_hash(coinbase.hash(false)); + const auto tx0_hash = encode_hash(coinbase.hash(false)); const auto request = R"({"id":83,"method":"blockchain.transaction.get","params":["%1%",true]})" "\n"; - const auto response = get((boost::format(request) % tx_hash).str()); + const auto response = get((boost::format(request) % tx0_hash).str()); auto expected = value_from(bitcoind(coinbase)); BOOST_CHECK(expected.is_object()); @@ -150,6 +150,57 @@ BOOST_AUTO_TEST_CASE(electrum__blockchain_transaction_get__genesis_coinbase_verb } // blockchain.transaction.get_merkle + // blockchain.transaction.id_from_pos +BOOST_AUTO_TEST_CASE(electrum__blockchain_transaction_id_from_pos__genesis_coinbase_default__expected) +{ + BOOST_CHECK(handshake()); + + const auto& coinbase = *genesis.transactions_ptr()->front(); + const auto tx0_hash = encode_hash(coinbase.hash(false)); + const auto request = R"({"id":90,"method":"blockchain.transaction.id_from_pos","params":[0,0]})" "\n"; + const auto response = get(request); + BOOST_CHECK_EQUAL(response.at("result").as_string(), tx0_hash); +} + +BOOST_AUTO_TEST_CASE(electrum__blockchain_transaction_id_from_pos__coinbase_false__expected) +{ + BOOST_CHECK(handshake()); + + const auto& coinbase = *block2.transactions_ptr()->front(); + const auto tx0_hash = encode_hash(coinbase.hash(false)); + const auto request = R"({"id":91,"method":"blockchain.transaction.id_from_pos","params":[2,0,false]})" "\n"; + const auto response = get(request); + BOOST_CHECK_EQUAL(response.at("result").as_string(), tx0_hash); +} + +BOOST_AUTO_TEST_CASE(electrum__blockchain_transaction_id_from_pos__merkle_proof_one_tx__empty) +{ + BOOST_CHECK(handshake()); + + const auto& coinbase = *block9.transactions_ptr()->front(); + const auto tx0_hash = encode_hash(coinbase.hash(false)); + const auto request = R"({"id":92,"method":"blockchain.transaction.id_from_pos","params":[9,0,true]})" "\n"; + const auto& object = get(request).at("result").as_object(); + BOOST_CHECK_EQUAL(object.at("tx_hash").as_string(), tx0_hash); + BOOST_CHECK(object.at("merkle").as_array().empty()); +} + +BOOST_AUTO_TEST_CASE(electrum__blockchain_transaction_id_from_pos__missing_block__not_found) +{ + BOOST_CHECK(handshake()); + + const auto request = R"({"id":93,"method":"blockchain.transaction.id_from_pos","params":[11,0]})" "\n"; + BOOST_CHECK_EQUAL(get(request).at("error").as_object().at("code").as_int64(), not_found.value()); +} + +BOOST_AUTO_TEST_CASE(electrum__blockchain_transaction_id_from_pos__missing_position__not_found) +{ + BOOST_CHECK(handshake()); + + const auto request = R"({"id":94,"method":"blockchain.transaction.id_from_pos","params":[0,1]})" "\n"; + BOOST_CHECK_EQUAL(get(request).at("error").as_object().at("code").as_int64(), not_found.value()); +} + BOOST_AUTO_TEST_SUITE_END()