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 @@
+
diff --git a/include/bitcoin/server/interfaces/native.hpp b/include/bitcoin/server/interfaces/native.hpp
index 5efb5e57..40805491 100644
--- a/include/bitcoin/server/interfaces/native.hpp
+++ b/include/bitcoin/server/interfaces/native.hpp
@@ -72,7 +72,9 @@ 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<"inscription", uint8_t, uint8_t, system::hash_cptr, uint32_t>{ "version", "media", "hash", "index" }
};
template
@@ -126,6 +128,8 @@ struct native_methods
// TODO: move to admin interface (security).
using log_subscribe = at<33>;
using event_subscribe = at<34>;
+
+ using inscription = at<35>;
};
/// ?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..80cb3980 100644
--- a/include/bitcoin/server/protocols/protocol_native.hpp
+++ b/include/bitcoin/server/protocols/protocol_native.hpp
@@ -177,6 +177,10 @@ 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_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..72afd769 100644
--- a/src/parsers/native_target.cpp
+++ b/src/parsers/native_target.cpp
@@ -318,6 +318,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..c420c94e 100644
--- a/src/protocols/native/protocol_native.cpp
+++ b/src/protocols/native/protocol_native.cpp
@@ -93,6 +93,9 @@ 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);
+
+ // Inscription methods.
+ 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..3cbacba3 100644
--- a/src/protocols/native/protocol_native_output.cpp
+++ b/src/protocols/native/protocol_native_output.cpp
@@ -205,6 +205,7 @@ bool protocol_native::handle_get_output_subscribe(const code& ec,
return {};
}
+
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
{
diff --git a/test/parsers/native_target.cpp b/test/parsers/native_target.cpp
index ba5d4c8a..66f124a3 100644
--- a/test/parsers/native_target.cpp
+++ b/test/parsers/native_target.cpp
@@ -1341,4 +1341,68 @@ BOOST_AUTO_TEST_CASE(parsers__native_target__block_details_hash_extra_segment__e
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..cb48ed32 100644
--- a/test/protocols/native/native_output.cpp
+++ b/test/protocols/native/native_output.cpp
@@ -21,4 +21,31 @@
BOOST_FIXTURE_TEST_SUITE(native_tests, native_ten_block_setup_fixture)
+using namespace system;
+using namespace boost::beast;
+
+// 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()