From b53d457b43cd3011d582a78c432961b87d1d90e0 Mon Sep 17 00:00:00 2001 From: Kevin Abramczyk Date: Wed, 13 May 2026 21:34:36 -0400 Subject: [PATCH 1/4] Add output/op_return and inscription native endpoints --- include/bitcoin/server/interfaces/native.hpp | 9 +- .../server/protocols/protocol_html.hpp | 3 + .../server/protocols/protocol_native.hpp | 8 + src/parsers/native_target.cpp | 22 ++ src/protocols/native/protocol_native.cpp | 4 + .../native/protocol_native_inscription.cpp | 288 ++++++++++++++++++ .../native/protocol_native_output.cpp | 132 ++++++++ src/protocols/protocol_html.cpp | 13 + 8 files changed, 478 insertions(+), 1 deletion(-) create mode 100644 src/protocols/native/protocol_native_inscription.cpp diff --git a/include/bitcoin/server/interfaces/native.hpp b/include/bitcoin/server/interfaces/native.hpp index 5efb5e57..04361b48 100644 --- a/include/bitcoin/server/interfaces/native.hpp +++ b/include/bitcoin/server/interfaces/native.hpp @@ -72,7 +72,11 @@ struct native_methods // TODO: move to admin interface (security). method<"log_subscribe", uint8_t, uint8_t>{ "version", "media" }, - method<"event_subscribe", uint8_t, uint8_t>{ "version", "media" } + method<"event_subscribe", uint8_t, uint8_t>{ "version", "media" }, + + method<"output_op_return", uint8_t, uint8_t, system::hash_cptr, uint32_t>{ "version", "media", "hash", "index" }, + + method<"inscription", uint8_t, uint8_t, system::hash_cptr, uint32_t>{ "version", "media", "hash", "index" } }; template @@ -126,6 +130,9 @@ struct native_methods // TODO: move to admin interface (security). using log_subscribe = at<33>; using event_subscribe = at<34>; + + using output_op_return = at<35>; + using inscription = at<36>; }; /// ?format=data|text|json (via query string). diff --git a/include/bitcoin/server/protocols/protocol_html.hpp b/include/bitcoin/server/protocols/protocol_html.hpp index 3e530b4d..88d7f35d 100644 --- a/include/bitcoin/server/protocols/protocol_html.hpp +++ b/include/bitcoin/server/protocols/protocol_html.hpp @@ -65,6 +65,9 @@ class BCS_API protocol_html const network::http::request& request={}) NOEXCEPT; virtual void send_chunk(system::data_chunk&& bytes, const network::http::request& request={}) NOEXCEPT; + virtual void send_typed_chunk(system::data_chunk&& bytes, + network::http::media_type type, + const network::http::request& request={}) NOEXCEPT; virtual void send_file(network::http::file&& file, network::http::media_type type, const network::http::request& request={}) NOEXCEPT; diff --git a/include/bitcoin/server/protocols/protocol_native.hpp b/include/bitcoin/server/protocols/protocol_native.hpp index 5c3cafed..2c5afebd 100644 --- a/include/bitcoin/server/protocols/protocol_native.hpp +++ b/include/bitcoin/server/protocols/protocol_native.hpp @@ -177,6 +177,14 @@ class BCS_API protocol_native bool handle_get_event_subscribe(const code& ec, interface::event_subscribe, uint8_t version, uint8_t media) NOEXCEPT; + bool handle_get_output_op_return(const code& ec, interface::output_op_return, + uint8_t version, uint8_t media, const system::hash_cptr& hash, + uint32_t index) NOEXCEPT; + + bool handle_get_inscription(const code& ec, interface::inscription, + uint8_t version, uint8_t media, const system::hash_cptr& hash, + uint32_t index) NOEXCEPT; + private: using media_type = network::http::media_type; static constexpr uint8_t text = to_value(media_type::text_plain); diff --git a/src/parsers/native_target.cpp b/src/parsers/native_target.cpp index 98448854..5f48303d 100644 --- a/src/parsers/native_target.cpp +++ b/src/parsers/native_target.cpp @@ -190,6 +190,8 @@ code native_target(request_t& out, const std::string_view& path) NOEXCEPT method = "output_spender"; else if (subcomponent == "spenders") method = "output_spenders"; + else if (subcomponent == "op_return") + method = "output_op_return"; else return error::invalid_subcomponent; } @@ -318,6 +320,26 @@ code native_target(request_t& out, const std::string_view& path) NOEXCEPT return error::invalid_component; } } + else if (target == "inscription") + { + if (segment == segments.size()) + return error::missing_hash; + + const auto hash = to_hash(segments[segment++]); + if (!hash) return error::invalid_hash; + + params["hash"] = hash; + if (segment == segments.size()) + return error::missing_position; + + const auto component = segments[segment++]; + uint32_t index{}; + if (!to_number(index, component)) + return error::invalid_number; + + params["index"] = index; + method = "inscription"; + } else { return error::invalid_target; diff --git a/src/protocols/native/protocol_native.cpp b/src/protocols/native/protocol_native.cpp index 939cf34b..f57da8d8 100644 --- a/src/protocols/native/protocol_native.cpp +++ b/src/protocols/native/protocol_native.cpp @@ -93,6 +93,10 @@ void protocol_native::start() NOEXCEPT // Admin endpoint methods (TODO: move to admin interface). SUBSCRIBE_NATIVE(handle_get_log_subscribe, _1, _2, _3, _4); SUBSCRIBE_NATIVE(handle_get_event_subscribe, _1, _2, _3, _4); + + // OP_RETURN and inscription methods. + SUBSCRIBE_NATIVE(handle_get_output_op_return, _1, _2, _3, _4, _5, _6); + SUBSCRIBE_NATIVE(handle_get_inscription, _1, _2, _3, _4, _5, _6); protocol_html::start(); } diff --git a/src/protocols/native/protocol_native_inscription.cpp b/src/protocols/native/protocol_native_inscription.cpp new file mode 100644 index 00000000..1bca3617 --- /dev/null +++ b/src/protocols/native/protocol_native_inscription.cpp @@ -0,0 +1,288 @@ +/** + * Copyright (c) 2011-2026 libbitcoin developers (see AUTHORS) + * + * This file is part of libbitcoin. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +#include + +#include +#include +#include +#include +#include + +namespace libbitcoin { +namespace server { + +using namespace system; +using namespace network::http; + +BC_PUSH_WARNING(NO_INCOMPLETE_SWITCH) +BC_PUSH_WARNING(NO_THROW_IN_NOEXCEPT) + +// Inscription parsing helpers. +// ---------------------------------------------------------------------------- +// private/static + +// Inscription mark bytes: OP_FALSE OP_IF OP_PUSHBYTES_3 "ord" +// Matches ordpool-parser's INSCRIPTION_MARK constant. +// https://github.com/ordpool-space/ordpool-parser +static constexpr uint8_t inscription_mark[] = + { 0x00, 0x63, 0x03, 0x6f, 0x72, 0x64 }; +static constexpr size_t inscription_mark_size = 6; + +// Field tags per the Ordinals protocol. +static constexpr uint8_t field_content_type = 0x01; +static constexpr uint8_t field_content_encoding = 0x09; + +// OP_0 / separator and OP_ENDIF byte values. +static constexpr uint8_t op_0 = 0x00; +static constexpr uint8_t op_endif = 0x68; + +struct inscription_t +{ + std::string content_type; + std::string content_encoding; // optional, e.g. "br" or "gzip" + data_chunk body; +}; + +// Read one pushdata operation at position p; advances p past the push. +// Returns data bytes for the push, or nullopt for non-pushdata opcodes. +static std::optional read_pushdata(const data_chunk& raw, + size_t& p) NOEXCEPT +{ + if (p >= raw.size()) + return std::nullopt; + + const auto op = static_cast(raw[p++]); + + // push_size_0..push_size_75: opcode value is the byte count. + if (op <= 0x4b) + { + if (p + op > raw.size()) return std::nullopt; + data_chunk out(raw.begin() + p, raw.begin() + p + op); + p += op; + return out; + } + + // OP_PUSHDATA1 (0x4c): next byte is size. + if (op == 0x4c) + { + if (p >= raw.size()) return std::nullopt; + const size_t size = raw[p++]; + if (p + size > raw.size()) return std::nullopt; + data_chunk out(raw.begin() + p, raw.begin() + p + size); + p += size; + return out; + } + + // OP_PUSHDATA2 (0x4d): next 2 bytes LE is size. + if (op == 0x4d) + { + if (p + 2 > raw.size()) return std::nullopt; + const size_t size = raw[p] | (static_cast(raw[p + 1]) << 8); + p += 2; + if (p + size > raw.size()) return std::nullopt; + data_chunk out(raw.begin() + p, raw.begin() + p + size); + p += size; + return out; + } + + // OP_PUSHDATA4 (0x4e): next 4 bytes LE is size. + if (op == 0x4e) + { + if (p + 4 > raw.size()) return std::nullopt; + const size_t size = + raw[p] | + (static_cast(raw[p + 1]) << 8) | + (static_cast(raw[p + 2]) << 16) | + (static_cast(raw[p + 3]) << 24); + p += 4; + if (p + size > raw.size()) return std::nullopt; + data_chunk out(raw.begin() + p, raw.begin() + p + size); + p += size; + return out; + } + + // Non-pushdata opcode: back up and return nullopt. + --p; + return std::nullopt; +} + +// Search raw script bytes for the inscription mark. +// Returns the offset of the mark, or raw.size() if not found. +static size_t find_inscription_mark(const data_chunk& raw) NOEXCEPT +{ + if (raw.size() < inscription_mark_size) + return raw.size(); + + for (size_t i = 0; i <= raw.size() - inscription_mark_size; ++i) + { + if (std::memcmp(raw.data() + i, inscription_mark, + inscription_mark_size) == 0) + return i; + } + return raw.size(); +} + +static std::optional parse_inscription( + const data_chunk& script_bytes) NOEXCEPT +{ + const auto mark = find_inscription_mark(script_bytes); + if (mark >= script_bytes.size()) + return std::nullopt; + + // Advance past OP_FALSE OP_IF OP_PUSHBYTES_3 "ord". + size_t p = mark + inscription_mark_size; + + inscription_t result; + + // Parse field-value pairs until OP_0 separator or OP_ENDIF. + while (p < script_bytes.size()) + { + const auto peek = script_bytes[p]; + if (peek == op_0) { ++p; break; } // separator: body follows + if (peek == op_endif) return result; // no body + + const auto tag = read_pushdata(script_bytes, p); + if (!tag.has_value()) return result; + + const auto val = read_pushdata(script_bytes, p); + if (!val.has_value()) return result; + + if (tag->size() == 1) + { + switch ((*tag)[0]) + { + case field_content_type: + result.content_type = + std::string(val->begin(), val->end()); + break; + case field_content_encoding: + result.content_encoding = + std::string(val->begin(), val->end()); + break; + default: + break; + } + } + } + + // Collect body chunks until OP_ENDIF. + while (p < script_bytes.size()) + { + if (script_bytes[p] == op_endif) break; + + const auto chunk = read_pushdata(script_bytes, p); + if (!chunk.has_value()) break; + result.body.insert(result.body.end(), chunk->begin(), chunk->end()); + } + + if (result.content_type.empty() && result.body.empty()) + return std::nullopt; + + return result; +} + +// Extract the tapscript leaf from a taproot input witness (BIP-341). +// Returns nullptr if the witness doesn't look like a tapscript spend. +static const data_chunk* tapscript_from_witness( + const chain::witness& witness) NOEXCEPT +{ + const auto& stack = witness.stack(); + if (stack.size() < 2) + return nullptr; + + const bool has_annex = chain::annex::is_annex_pattern(stack); + + const size_t effective = stack.size() - (has_annex ? 1 : 0); + if (effective < 2) + return nullptr; + + // Tapscript is second-to-last (effective index = effective - 2). + return stack[effective - 2].get(); +} + +// Handler. +// ---------------------------------------------------------------------------- + +bool protocol_native::handle_get_inscription(const code& ec, + interface::inscription, uint8_t, uint8_t media, + const hash_cptr& hash, uint32_t index) NOEXCEPT +{ + if (stopped(ec)) + return false; + + const auto& query = archive(); + const auto witness = query.get_witness( + query.to_point(query.to_tx(*hash), index)); + + if (is_null(witness) || !witness->is_valid()) + { + send_not_found(); + return true; + } + + const auto* script_bytes = tapscript_from_witness(*witness); + if (script_bytes == nullptr) + { + send_not_found(); + return true; + } + + const auto inscription = parse_inscription(*script_bytes); + if (!inscription.has_value()) + { + send_not_found(); + return true; + } + + switch (media) + { + case data: + { + // Serve raw content bytes with the inscription's content-type. + const auto type = content_media_type(inscription->content_type, + media_type::application_octet_stream); + send_typed_chunk(data_chunk{ inscription->body }, type); + return true; + } + case text: + send_text(encode_base16(inscription->body)); + return true; + case json: + { + boost::json::object object; + object.emplace("content_type", inscription->content_type); + object.emplace("content_base64", encode_base64(inscription->body)); + object.emplace("size", inscription->body.size()); + if (!inscription->content_encoding.empty()) + object.emplace("content_encoding", inscription->content_encoding); + + send_json(std::move(object), two * inscription->body.size() + 128); + return true; + } + } + + send_not_found(); + return true; +} + +BC_POP_WARNING() +BC_POP_WARNING() + +} // namespace server +} // namespace libbitcoin diff --git a/src/protocols/native/protocol_native_output.cpp b/src/protocols/native/protocol_native_output.cpp index 4b6f0e13..20eabd68 100644 --- a/src/protocols/native/protocol_native_output.cpp +++ b/src/protocols/native/protocol_native_output.cpp @@ -19,6 +19,8 @@ #include #include +#include +#include #include namespace libbitcoin { @@ -205,6 +207,136 @@ bool protocol_native::handle_get_output_subscribe(const code& ec, return {}; } +// OP_RETURN helpers. +// ---------------------------------------------------------------------------- +// private/static + +static bool starts_with_bytes(const data_chunk& payload, + const uint8_t* prefix, size_t prefix_size) NOEXCEPT +{ + return payload.size() >= prefix_size && + std::equal(prefix, prefix + prefix_size, payload.begin()); +} + +static const char* detect_op_return_protocol( + const chain::operations& ops, const data_chunk& payload) NOEXCEPT +{ + // Runes: OP_RETURN OP_13 — identified by opcode, not payload bytes. + if (ops.size() >= 2 && ops[1].code() == chain::opcode::push_positive_13) + return "Runes"; + + static constexpr uint8_t omni[] = { 0x6f, 0x6d, 0x6e, 0x69 }; + static constexpr uint8_t counterparty[] = { 0x43, 0x4e, 0x54, 0x52, 0x50 }; + static constexpr uint8_t stamps[] = { 0x53, 0x54, 0x4d, 0x50 }; + static constexpr uint8_t bip320[] = { 0x42, 0x49, 0x50, 0x33, 0x32, 0x30 }; + + if (starts_with_bytes(payload, omni, sizeof(omni))) + return "Omni Layer"; + if (starts_with_bytes(payload, counterparty, sizeof(counterparty))) + return "Counterparty"; + if (starts_with_bytes(payload, stamps, sizeof(stamps))) + return "Stamps"; + if (starts_with_bytes(payload, bip320, sizeof(bip320))) + return "BIP-320"; + return nullptr; +} + +static std::optional try_utf8( + const data_chunk& payload) NOEXCEPT +{ + size_t i = 0; + while (i < payload.size()) + { + const auto byte = static_cast(payload[i]); + size_t extra; + if ((byte & 0x80) == 0x00) extra = 0; + else if ((byte & 0xe0) == 0xc0) extra = 1; + else if ((byte & 0xf0) == 0xe0) extra = 2; + else if ((byte & 0xf8) == 0xf0) extra = 3; + else return std::nullopt; + + if (i + extra >= payload.size() && extra > 0) return std::nullopt; + for (size_t j = 1; j <= extra; ++j) + if ((static_cast(payload[i + j]) & 0xc0) != 0x80) + return std::nullopt; + i += 1 + extra; + } + return std::string(payload.begin(), payload.end()); +} + +// Handler. +// ---------------------------------------------------------------------------- + +bool protocol_native::handle_get_output_op_return(const code& ec, + interface::output_op_return, uint8_t, uint8_t media, + const hash_cptr& hash, uint32_t index) NOEXCEPT +{ + if (stopped(ec)) + return false; + + const auto& query = archive(); + const auto script = query.get_output_script( + query.to_output(query.to_tx(*hash), index)); + + if (!script) + { + send_not_found(); + return true; + } + + const auto& ops = script->ops(); + if (ops.empty() || ops[0].code() != chain::opcode::op_return) + { + send_not_found(); + return true; + } + + // Collect payload from all pushdata ops after OP_RETURN. + data_chunk payload{}; + for (size_t i = 1; i < ops.size(); ++i) + { + const auto& bytes = ops[i].data(); + payload.insert(payload.end(), bytes.begin(), bytes.end()); + } + + if (payload.empty()) + { + send_not_found(); + return true; + } + + switch (media) + { + case data: + send_chunk(data_chunk(payload.begin(), payload.end())); + return true; + case text: + send_text(encode_base16(payload)); + return true; + case json: + { + boost::json::object object; + object.emplace("hex", encode_base16(payload)); + object.emplace("base64", encode_base64(payload)); + object.emplace("size", payload.size()); + + const auto* protocol = detect_op_return_protocol(ops, payload); + if (protocol != nullptr) + object.emplace("protocol", protocol); + + const auto utf8 = try_utf8(payload); + if (utf8.has_value()) + object.emplace("utf8", utf8.value()); + + send_json(std::move(object), two * payload.size() + 64); + return true; + } + } + + send_not_found(); + return true; +} + BC_POP_WARNING() BC_POP_WARNING() diff --git a/src/protocols/protocol_html.cpp b/src/protocols/protocol_html.cpp index 26e04e61..0c0ea361 100644 --- a/src/protocols/protocol_html.cpp +++ b/src/protocols/protocol_html.cpp @@ -217,6 +217,19 @@ void protocol_html::send_chunk(system::data_chunk&& bytes, SEND(std::move(response), handle_complete, _1, error::success); } +void protocol_html::send_typed_chunk(system::data_chunk&& bytes, + media_type type, const request& request) NOEXCEPT +{ + BC_ASSERT(stranded()); + response response{ status::ok, request.version() }; + add_common_headers(response, request); + add_access_control_headers(response, request); + response.set(field::content_type, from_media_type(type)); + response.body() = std::move(bytes); + response.prepare_payload(); + SEND(std::move(response), handle_complete, _1, error::success); +} + void protocol_html::send_file(file&& file, media_type type, const request& request) NOEXCEPT { From 8c4adbb6db7e9e56b63fcbec45be7a00f85d47af Mon Sep 17 00:00:00 2001 From: Kevin Abramczyk Date: Wed, 13 May 2026 21:34:58 -0400 Subject: [PATCH 2/4] Add tests for output/op_return and inscription endpoints --- test/parsers/native_target.cpp | 102 ++++++++++++++++++++++++ test/protocols/native/native_output.cpp | 51 ++++++++++++ 2 files changed, 153 insertions(+) diff --git a/test/parsers/native_target.cpp b/test/parsers/native_target.cpp index ba5d4c8a..992468ba 100644 --- a/test/parsers/native_target.cpp +++ b/test/parsers/native_target.cpp @@ -1341,4 +1341,106 @@ BOOST_AUTO_TEST_CASE(parsers__native_target__block_details_hash_extra_segment__e BOOST_REQUIRE_EQUAL(native_target(out, path), server::error::extra_segment); } +// output_op_return + +BOOST_AUTO_TEST_CASE(parsers__native_target__output_op_return_valid__expected) +{ + const std::string path = "/v255/output/0000000000000000000000000000000000000000000000000000000000000042/3/op_return"; + + request_t request{}; + BOOST_REQUIRE(!native_target(request, path)); + BOOST_REQUIRE_EQUAL(request.method, "output_op_return"); + BOOST_REQUIRE(request.params.has_value()); + + const auto& params = request.params.value(); + BOOST_REQUIRE(std::holds_alternative(params)); + + const auto& object = std::get(request.params.value()); + BOOST_REQUIRE_EQUAL(object.size(), 3u); + + const auto version = std::get(object.at("version").value()); + BOOST_REQUIRE_EQUAL(version, 255u); + + const auto& any = std::get(object.at("hash").value()); + BOOST_REQUIRE(any.holds_alternative()); + + const auto& hash_cptr = any.get(); + BOOST_REQUIRE(hash_cptr); + BOOST_REQUIRE_EQUAL(to_uintx(*hash_cptr), uint256_t{ 0x42 }); + + const auto index = std::get(object.at("index").value()); + BOOST_REQUIRE_EQUAL(index, 3u); +} + +BOOST_AUTO_TEST_CASE(parsers__native_target__output_op_return_extra_segment__extra_segment) +{ + const std::string path = "/v3/output/0000000000000000000000000000000000000000000000000000000000000000/3/op_return/extra"; + request_t out{}; + BOOST_REQUIRE_EQUAL(native_target(out, path), server::error::extra_segment); +} + +// inscription + +BOOST_AUTO_TEST_CASE(parsers__native_target__inscription_valid__expected) +{ + const std::string path = "/v42/inscription/0000000000000000000000000000000000000000000000000000000000000042/0"; + + request_t request{}; + BOOST_REQUIRE(!native_target(request, path)); + BOOST_REQUIRE_EQUAL(request.method, "inscription"); + BOOST_REQUIRE(request.params.has_value()); + + const auto& params = request.params.value(); + BOOST_REQUIRE(std::holds_alternative(params)); + + const auto& object = std::get(request.params.value()); + BOOST_REQUIRE_EQUAL(object.size(), 3u); + + const auto version = std::get(object.at("version").value()); + BOOST_REQUIRE_EQUAL(version, 42u); + + const auto& any = std::get(object.at("hash").value()); + BOOST_REQUIRE(any.holds_alternative()); + + const auto& hash_cptr = any.get(); + BOOST_REQUIRE(hash_cptr); + BOOST_REQUIRE_EQUAL(to_uintx(*hash_cptr), uint256_t{ 0x42 }); + + const auto index = std::get(object.at("index").value()); + BOOST_REQUIRE_EQUAL(index, 0u); +} + +BOOST_AUTO_TEST_CASE(parsers__native_target__inscription_missing_hash__missing_hash) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(native_target(out, "/v3/inscription"), server::error::missing_hash); +} + +BOOST_AUTO_TEST_CASE(parsers__native_target__inscription_invalid_hash__invalid_hash) +{ + request_t out{}; + BOOST_REQUIRE_EQUAL(native_target(out, "/v3/inscription/invalidhex/0"), server::error::invalid_hash); +} + +BOOST_AUTO_TEST_CASE(parsers__native_target__inscription_missing_position__missing_position) +{ + const std::string path = "/v3/inscription/0000000000000000000000000000000000000000000000000000000000000000"; + request_t out{}; + BOOST_REQUIRE_EQUAL(native_target(out, path), server::error::missing_position); +} + +BOOST_AUTO_TEST_CASE(parsers__native_target__inscription_invalid_index__invalid_number) +{ + const std::string path = "/v3/inscription/0000000000000000000000000000000000000000000000000000000000000000/invalid"; + request_t out{}; + BOOST_REQUIRE_EQUAL(native_target(out, path), server::error::invalid_number); +} + +BOOST_AUTO_TEST_CASE(parsers__native_target__inscription_extra_segment__extra_segment) +{ + const std::string path = "/v3/inscription/0000000000000000000000000000000000000000000000000000000000000000/0/extra"; + request_t out{}; + BOOST_REQUIRE_EQUAL(native_target(out, path), server::error::extra_segment); +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/test/protocols/native/native_output.cpp b/test/protocols/native/native_output.cpp index d0c34c1e..7ef4c943 100644 --- a/test/protocols/native/native_output.cpp +++ b/test/protocols/native/native_output.cpp @@ -21,4 +21,55 @@ BOOST_FIXTURE_TEST_SUITE(native_tests, native_ten_block_setup_fixture) +using namespace system; +using namespace boost::beast; + +// output_op_return (http) +// ---------------------------------------------------------------------------- + +BOOST_AUTO_TEST_CASE(native__output_op_return__unknown_tx__not_found) +{ + const auto status = get_status( + "/v1/output/0000000000000000000000000000000000000000000000000000000000000000/0/op_return?format=json"); + BOOST_REQUIRE_EQUAL(status, http::status::not_found); +} + +BOOST_AUTO_TEST_CASE(native__output_op_return__unknown_tx__not_found_text) +{ + const auto status = get_status( + "/v1/output/0000000000000000000000000000000000000000000000000000000000000000/0/op_return?format=text"); + BOOST_REQUIRE_EQUAL(status, http::status::not_found); +} + +BOOST_AUTO_TEST_CASE(native__output_op_return__unknown_tx__not_found_data) +{ + const auto status = get_status( + "/v1/output/0000000000000000000000000000000000000000000000000000000000000000/0/op_return?format=data"); + BOOST_REQUIRE_EQUAL(status, http::status::not_found); +} + +// inscription (http) +// ---------------------------------------------------------------------------- + +BOOST_AUTO_TEST_CASE(native__inscription__unknown_tx__not_found) +{ + const auto status = get_status( + "/v1/inscription/0000000000000000000000000000000000000000000000000000000000000000/0?format=json"); + BOOST_REQUIRE_EQUAL(status, http::status::not_found); +} + +BOOST_AUTO_TEST_CASE(native__inscription__unknown_tx__not_found_text) +{ + const auto status = get_status( + "/v1/inscription/0000000000000000000000000000000000000000000000000000000000000000/0?format=text"); + BOOST_REQUIRE_EQUAL(status, http::status::not_found); +} + +BOOST_AUTO_TEST_CASE(native__inscription__unknown_tx__not_found_data) +{ + const auto status = get_status( + "/v1/inscription/0000000000000000000000000000000000000000000000000000000000000000/0?format=data"); + BOOST_REQUIRE_EQUAL(status, http::status::not_found); +} + BOOST_AUTO_TEST_SUITE_END() From e82cf2183015e22c26efd21ff3fa8ce9b9a13c41 Mon Sep 17 00:00:00 2001 From: Kevin Abramczyk Date: Wed, 13 May 2026 22:12:33 -0400 Subject: [PATCH 3/4] Add protocol_native_inscription.cpp to build systems --- Makefile.am | 1 + builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj | 1 + builds/msvc/vs2026/libbitcoin-server/libbitcoin-server.vcxproj | 1 + 3 files changed, 3 insertions(+) diff --git a/Makefile.am b/Makefile.am index 9f5b7bed..c03a4091 100644 --- a/Makefile.am +++ b/Makefile.am @@ -66,6 +66,7 @@ src_libbitcoin_server_la_SOURCES = \ src/protocols/native/protocol_native_address.cpp \ src/protocols/native/protocol_native_block.cpp \ src/protocols/native/protocol_native_input.cpp \ + src/protocols/native/protocol_native_inscription.cpp \ src/protocols/native/protocol_native_output.cpp \ src/protocols/native/protocol_native_tx.cpp \ src/protocols/stratum_v1/protocol_stratum_v1.cpp diff --git a/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj b/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj index feb21832..c614a59b 100644 --- a/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj +++ b/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj @@ -148,6 +148,7 @@ + diff --git a/builds/msvc/vs2026/libbitcoin-server/libbitcoin-server.vcxproj b/builds/msvc/vs2026/libbitcoin-server/libbitcoin-server.vcxproj index bd7a8a8c..8e160fae 100644 --- a/builds/msvc/vs2026/libbitcoin-server/libbitcoin-server.vcxproj +++ b/builds/msvc/vs2026/libbitcoin-server/libbitcoin-server.vcxproj @@ -148,6 +148,7 @@ + From a0747e97661f480cb7d536aff5bd1ba664ba8d21 Mon Sep 17 00:00:00 2001 From: Kevin Abramczyk Date: Wed, 13 May 2026 22:45:46 -0400 Subject: [PATCH 4/4] Remove output/op_return endpoint --- include/bitcoin/server/interfaces/native.hpp | 5 +- .../server/protocols/protocol_native.hpp | 4 - src/parsers/native_target.cpp | 2 - src/protocols/native/protocol_native.cpp | 3 +- .../native/protocol_native_output.cpp | 131 ------------------ test/parsers/native_target.cpp | 38 ----- test/protocols/native/native_output.cpp | 24 ---- 7 files changed, 2 insertions(+), 205 deletions(-) diff --git a/include/bitcoin/server/interfaces/native.hpp b/include/bitcoin/server/interfaces/native.hpp index 04361b48..40805491 100644 --- a/include/bitcoin/server/interfaces/native.hpp +++ b/include/bitcoin/server/interfaces/native.hpp @@ -74,8 +74,6 @@ struct native_methods method<"log_subscribe", uint8_t, uint8_t>{ "version", "media" }, method<"event_subscribe", uint8_t, uint8_t>{ "version", "media" }, - method<"output_op_return", uint8_t, uint8_t, system::hash_cptr, uint32_t>{ "version", "media", "hash", "index" }, - method<"inscription", uint8_t, uint8_t, system::hash_cptr, uint32_t>{ "version", "media", "hash", "index" } }; @@ -131,8 +129,7 @@ struct native_methods using log_subscribe = at<33>; using event_subscribe = at<34>; - using output_op_return = at<35>; - using inscription = at<36>; + using inscription = at<35>; }; /// ?format=data|text|json (via query string). diff --git a/include/bitcoin/server/protocols/protocol_native.hpp b/include/bitcoin/server/protocols/protocol_native.hpp index 2c5afebd..80cb3980 100644 --- a/include/bitcoin/server/protocols/protocol_native.hpp +++ b/include/bitcoin/server/protocols/protocol_native.hpp @@ -177,10 +177,6 @@ class BCS_API protocol_native bool handle_get_event_subscribe(const code& ec, interface::event_subscribe, uint8_t version, uint8_t media) NOEXCEPT; - bool handle_get_output_op_return(const code& ec, interface::output_op_return, - uint8_t version, uint8_t media, const system::hash_cptr& hash, - uint32_t index) NOEXCEPT; - bool handle_get_inscription(const code& ec, interface::inscription, uint8_t version, uint8_t media, const system::hash_cptr& hash, uint32_t index) NOEXCEPT; diff --git a/src/parsers/native_target.cpp b/src/parsers/native_target.cpp index 5f48303d..72afd769 100644 --- a/src/parsers/native_target.cpp +++ b/src/parsers/native_target.cpp @@ -190,8 +190,6 @@ code native_target(request_t& out, const std::string_view& path) NOEXCEPT method = "output_spender"; else if (subcomponent == "spenders") method = "output_spenders"; - else if (subcomponent == "op_return") - method = "output_op_return"; else return error::invalid_subcomponent; } diff --git a/src/protocols/native/protocol_native.cpp b/src/protocols/native/protocol_native.cpp index f57da8d8..c420c94e 100644 --- a/src/protocols/native/protocol_native.cpp +++ b/src/protocols/native/protocol_native.cpp @@ -94,8 +94,7 @@ void protocol_native::start() NOEXCEPT SUBSCRIBE_NATIVE(handle_get_log_subscribe, _1, _2, _3, _4); SUBSCRIBE_NATIVE(handle_get_event_subscribe, _1, _2, _3, _4); - // OP_RETURN and inscription methods. - SUBSCRIBE_NATIVE(handle_get_output_op_return, _1, _2, _3, _4, _5, _6); + // Inscription methods. SUBSCRIBE_NATIVE(handle_get_inscription, _1, _2, _3, _4, _5, _6); protocol_html::start(); } diff --git a/src/protocols/native/protocol_native_output.cpp b/src/protocols/native/protocol_native_output.cpp index 20eabd68..3cbacba3 100644 --- a/src/protocols/native/protocol_native_output.cpp +++ b/src/protocols/native/protocol_native_output.cpp @@ -19,8 +19,6 @@ #include #include -#include -#include #include namespace libbitcoin { @@ -207,135 +205,6 @@ bool protocol_native::handle_get_output_subscribe(const code& ec, return {}; } -// OP_RETURN helpers. -// ---------------------------------------------------------------------------- -// private/static - -static bool starts_with_bytes(const data_chunk& payload, - const uint8_t* prefix, size_t prefix_size) NOEXCEPT -{ - return payload.size() >= prefix_size && - std::equal(prefix, prefix + prefix_size, payload.begin()); -} - -static const char* detect_op_return_protocol( - const chain::operations& ops, const data_chunk& payload) NOEXCEPT -{ - // Runes: OP_RETURN OP_13 — identified by opcode, not payload bytes. - if (ops.size() >= 2 && ops[1].code() == chain::opcode::push_positive_13) - return "Runes"; - - static constexpr uint8_t omni[] = { 0x6f, 0x6d, 0x6e, 0x69 }; - static constexpr uint8_t counterparty[] = { 0x43, 0x4e, 0x54, 0x52, 0x50 }; - static constexpr uint8_t stamps[] = { 0x53, 0x54, 0x4d, 0x50 }; - static constexpr uint8_t bip320[] = { 0x42, 0x49, 0x50, 0x33, 0x32, 0x30 }; - - if (starts_with_bytes(payload, omni, sizeof(omni))) - return "Omni Layer"; - if (starts_with_bytes(payload, counterparty, sizeof(counterparty))) - return "Counterparty"; - if (starts_with_bytes(payload, stamps, sizeof(stamps))) - return "Stamps"; - if (starts_with_bytes(payload, bip320, sizeof(bip320))) - return "BIP-320"; - return nullptr; -} - -static std::optional try_utf8( - const data_chunk& payload) NOEXCEPT -{ - size_t i = 0; - while (i < payload.size()) - { - const auto byte = static_cast(payload[i]); - size_t extra; - if ((byte & 0x80) == 0x00) extra = 0; - else if ((byte & 0xe0) == 0xc0) extra = 1; - else if ((byte & 0xf0) == 0xe0) extra = 2; - else if ((byte & 0xf8) == 0xf0) extra = 3; - else return std::nullopt; - - if (i + extra >= payload.size() && extra > 0) return std::nullopt; - for (size_t j = 1; j <= extra; ++j) - if ((static_cast(payload[i + j]) & 0xc0) != 0x80) - return std::nullopt; - i += 1 + extra; - } - return std::string(payload.begin(), payload.end()); -} - -// Handler. -// ---------------------------------------------------------------------------- - -bool protocol_native::handle_get_output_op_return(const code& ec, - interface::output_op_return, uint8_t, uint8_t media, - const hash_cptr& hash, uint32_t index) NOEXCEPT -{ - if (stopped(ec)) - return false; - - const auto& query = archive(); - const auto script = query.get_output_script( - query.to_output(query.to_tx(*hash), index)); - - if (!script) - { - send_not_found(); - return true; - } - - const auto& ops = script->ops(); - if (ops.empty() || ops[0].code() != chain::opcode::op_return) - { - send_not_found(); - return true; - } - - // Collect payload from all pushdata ops after OP_RETURN. - data_chunk payload{}; - for (size_t i = 1; i < ops.size(); ++i) - { - const auto& bytes = ops[i].data(); - payload.insert(payload.end(), bytes.begin(), bytes.end()); - } - - if (payload.empty()) - { - send_not_found(); - return true; - } - - switch (media) - { - case data: - send_chunk(data_chunk(payload.begin(), payload.end())); - return true; - case text: - send_text(encode_base16(payload)); - return true; - case json: - { - boost::json::object object; - object.emplace("hex", encode_base16(payload)); - object.emplace("base64", encode_base64(payload)); - object.emplace("size", payload.size()); - - const auto* protocol = detect_op_return_protocol(ops, payload); - if (protocol != nullptr) - object.emplace("protocol", protocol); - - const auto utf8 = try_utf8(payload); - if (utf8.has_value()) - object.emplace("utf8", utf8.value()); - - send_json(std::move(object), two * payload.size() + 64); - return true; - } - } - - send_not_found(); - return true; -} BC_POP_WARNING() BC_POP_WARNING() diff --git a/test/parsers/native_target.cpp b/test/parsers/native_target.cpp index 992468ba..66f124a3 100644 --- a/test/parsers/native_target.cpp +++ b/test/parsers/native_target.cpp @@ -1341,44 +1341,6 @@ BOOST_AUTO_TEST_CASE(parsers__native_target__block_details_hash_extra_segment__e BOOST_REQUIRE_EQUAL(native_target(out, path), server::error::extra_segment); } -// output_op_return - -BOOST_AUTO_TEST_CASE(parsers__native_target__output_op_return_valid__expected) -{ - const std::string path = "/v255/output/0000000000000000000000000000000000000000000000000000000000000042/3/op_return"; - - request_t request{}; - BOOST_REQUIRE(!native_target(request, path)); - BOOST_REQUIRE_EQUAL(request.method, "output_op_return"); - BOOST_REQUIRE(request.params.has_value()); - - const auto& params = request.params.value(); - BOOST_REQUIRE(std::holds_alternative(params)); - - const auto& object = std::get(request.params.value()); - BOOST_REQUIRE_EQUAL(object.size(), 3u); - - const auto version = std::get(object.at("version").value()); - BOOST_REQUIRE_EQUAL(version, 255u); - - const auto& any = std::get(object.at("hash").value()); - BOOST_REQUIRE(any.holds_alternative()); - - const auto& hash_cptr = any.get(); - BOOST_REQUIRE(hash_cptr); - BOOST_REQUIRE_EQUAL(to_uintx(*hash_cptr), uint256_t{ 0x42 }); - - const auto index = std::get(object.at("index").value()); - BOOST_REQUIRE_EQUAL(index, 3u); -} - -BOOST_AUTO_TEST_CASE(parsers__native_target__output_op_return_extra_segment__extra_segment) -{ - const std::string path = "/v3/output/0000000000000000000000000000000000000000000000000000000000000000/3/op_return/extra"; - request_t out{}; - BOOST_REQUIRE_EQUAL(native_target(out, path), server::error::extra_segment); -} - // inscription BOOST_AUTO_TEST_CASE(parsers__native_target__inscription_valid__expected) diff --git a/test/protocols/native/native_output.cpp b/test/protocols/native/native_output.cpp index 7ef4c943..cb48ed32 100644 --- a/test/protocols/native/native_output.cpp +++ b/test/protocols/native/native_output.cpp @@ -24,30 +24,6 @@ BOOST_FIXTURE_TEST_SUITE(native_tests, native_ten_block_setup_fixture) using namespace system; using namespace boost::beast; -// output_op_return (http) -// ---------------------------------------------------------------------------- - -BOOST_AUTO_TEST_CASE(native__output_op_return__unknown_tx__not_found) -{ - const auto status = get_status( - "/v1/output/0000000000000000000000000000000000000000000000000000000000000000/0/op_return?format=json"); - BOOST_REQUIRE_EQUAL(status, http::status::not_found); -} - -BOOST_AUTO_TEST_CASE(native__output_op_return__unknown_tx__not_found_text) -{ - const auto status = get_status( - "/v1/output/0000000000000000000000000000000000000000000000000000000000000000/0/op_return?format=text"); - BOOST_REQUIRE_EQUAL(status, http::status::not_found); -} - -BOOST_AUTO_TEST_CASE(native__output_op_return__unknown_tx__not_found_data) -{ - const auto status = get_status( - "/v1/output/0000000000000000000000000000000000000000000000000000000000000000/0/op_return?format=data"); - BOOST_REQUIRE_EQUAL(status, http::status::not_found); -} - // inscription (http) // ----------------------------------------------------------------------------