diff --git a/include/bitcoin/database/impl/query/archive.ipp b/include/bitcoin/database/impl/query/archive.ipp index 9a1b4927..edffe443 100644 --- a/include/bitcoin/database/impl/query/archive.ipp +++ b/include/bitcoin/database/impl/query/archive.ipp @@ -79,10 +79,35 @@ inline bool CLASS::is_coinbase(const tx_link& link) const NOEXCEPT return store_.tx.get(link, tx) && tx.coinbase; } +TEMPLATE +inline bool CLASS::is_malleated(const block& block) const NOEXCEPT +{ + // is_malleable_duplicate is always invalid, so never stored. + if (!block.is_malleable()) + return false; + + // This is invoked for all new blocks, most of which will not exist (cheap). + // TODO: determine if block is a bitwise match for a stored instance, where + // TODO: the stored and unassociated instances share the same block hash. + // TODO: if there is an associated instance this check need not be called. + // TODO: if the associated instance is matched then the assumption is that + // TODO: that instance is not malleated, but that may be later determined. + // TODO: so only disassociated instances are compared and otherwise false. + return true; +} + +TEMPLATE +inline bool CLASS::is_malleable(const header_link& link) const NOEXCEPT +{ + table::txs::get_malleable txs{}; + return store_.txs.get(to_txs_link(link), txs) && txs.malleable; +} + TEMPLATE inline bool CLASS::is_associated(const header_link& link) const NOEXCEPT { - return !link.is_terminal() && store_.txs.exists(link); + table::txs::get_associated txs{}; + return store_.txs.get(to_txs_link(link), txs) && txs.associated; } TEMPLATE @@ -762,13 +787,12 @@ txs_link CLASS::set_link(const transactions& txs, if (!push_link_value(links, set_link(*tx))) return {}; - // Malleable if all txs serialize to 64 bytes without witness. - const auto mutable_ = block::is_malleable64(txs); + const auto malleable = block::is_malleable64(txs); // ======================================================================== const auto scope = store_.get_transactor(); - return store_.txs.put_link(link, table::txs::slab{ {}, mutable_, links }); + return store_.txs.put_link(link, table::txs::slab{ {}, malleable, links }); // ======================================================================== } @@ -778,47 +802,15 @@ bool CLASS::dissasociate(const header_link& link) NOEXCEPT if (link.is_terminal()) return false; - // TODO: disassociate the header from its transactions. + const auto malleable = is_malleable(link); // ======================================================================== const auto scope = store_.get_transactor(); - return true; + return store_.txs.put_link(link, table::txs::slab{ {}, malleable, {} }); // ======================================================================== } -TEMPLATE -inline bool CLASS::is_malleated(const block& block) const NOEXCEPT -{ - // is_malleable_duplicate is always invalid, so never stored. - if (!block.is_malleable()) - return false; - - // This is invoked for all new blocks, most of which will not exist (cheap). - // TODO: determine if block is a bitwise match for a stored instance, where - // TODO: the stored and unassociated instances share the same block hash. - // TODO: if there is an associated instance this check need not be called. - // TODO: if the associated instance is matched then the assumption is that - // TODO: that instance is not malleated, but that may be later determined. - // TODO: so only disassociated instances are compared and otherwise false. - return true; -} - -TEMPLATE -inline bool CLASS::is_malleable(const header_link& link) const NOEXCEPT -{ - // Unknown treated as false, should not have been called unless associated. - if (!is_associated(link)) - return false; - - // Invoked after confirm failure (free). - // Invoked to not bypass checkpoints in validate/confirm (expensive). - // TODO: implement block.is_malleable() from the store. - // TODO: find any tx without a 64 byte non-witness size (false). - // TODO: or archive malleable bit with txs record. - return true; -} - } // namespace database } // namespace libbitcoin diff --git a/include/bitcoin/database/tables/archives/txs.hpp b/include/bitcoin/database/tables/archives/txs.hpp index 8cf4a92f..6c3f5183 100644 --- a/include/bitcoin/database/tables/archives/txs.hpp +++ b/include/bitcoin/database/tables/archives/txs.hpp @@ -123,6 +123,31 @@ struct txs tx::integer coinbase_fk{}; }; + + struct get_malleable + : public schema::txs + { + inline bool from_data(reader& source) NOEXCEPT + { + source.skip_bytes(schema::count_); + malleable = to_bool(source.read_byte()); + return source; + } + + bool malleable{}; + }; + + struct get_associated + : public schema::txs + { + inline bool from_data(reader& source) NOEXCEPT + { + associated = to_bool(source.read_little_endian()); + return source; + } + + bool associated{}; + }; }; } // namespace table diff --git a/test/query/archive.cpp b/test/query/archive.cpp index 6f87577c..09c8be89 100644 --- a/test/query/archive.cpp +++ b/test/query/archive.cpp @@ -716,8 +716,10 @@ BOOST_AUTO_TEST_CASE(query_archive__set_block_txs__get_block__expected) // Set header and then txs. BOOST_REQUIRE(!query.is_block(test::genesis.hash())); BOOST_REQUIRE(query.set(test::genesis.header(), test::context)); + BOOST_REQUIRE(!query.is_associated(0)); BOOST_REQUIRE(query.set(test::genesis)); BOOST_REQUIRE(query.is_block(test::genesis.hash())); + BOOST_REQUIRE(query.is_associated(0)); // Verify idempotentcy (these do not change store state). ////BOOST_REQUIRE(query.set(test::genesis.header(), test::context)); @@ -753,6 +755,11 @@ BOOST_AUTO_TEST_CASE(query_archive__set_block_txs__get_block__expected) const auto hashes = query.get_tx_keys(query.to_header(test::genesis.hash())); BOOST_REQUIRE_EQUAL(hashes.size(), 1u); BOOST_REQUIRE_EQUAL(hashes, test::genesis.transaction_hashes(false)); + + BOOST_REQUIRE(!query.is_malleable(0)); + BOOST_REQUIRE(query.dissasociate(0)); + BOOST_REQUIRE(!query.is_associated(0)); + BOOST_REQUIRE(!query.is_malleable(0)); } // Moved to protected, set_link(block) covers. @@ -885,7 +892,7 @@ BOOST_AUTO_TEST_CASE(query_archive__is_coinbase__coinbase__true) BOOST_REQUIRE(query.is_coinbase(3)); } -BOOST_AUTO_TEST_CASE(query_archive__is_coinbase__non_coinbase__true) +BOOST_AUTO_TEST_CASE(query_archive__is_coinbase__non_coinbase__false) { settings settings{}; settings.path = TEST_DIRECTORY; @@ -903,6 +910,54 @@ BOOST_AUTO_TEST_CASE(query_archive__is_coinbase__non_coinbase__true) BOOST_REQUIRE(!query.is_coinbase(42)); } +BOOST_AUTO_TEST_CASE(query_archive__is_malleable__malleable__true) +{ + using namespace system::chain; + transaction tx64 + { + 42, + inputs{ { point{}, script{ { opcode::dup, opcode::dup } }, 42 } }, + outputs{ { 42, script{ { opcode::dup, opcode::dup } } } }, + 42 + }; + + const block block1{ header{ 42, {}, {}, {}, {}, {} }, { tx64 } }; + const block block2{ header{ 43, {}, {}, {}, {}, {} }, { tx64, tx64 } }; + const block block3{ header{ 44, {}, {}, {}, {}, {} }, { tx64, tx64, tx64 } }; + + settings settings{}; + settings.path = TEST_DIRECTORY; + test::chunk_store store{ settings }; + test::query_accessor query{ store }; + BOOST_REQUIRE_EQUAL(store.create(events), error::success); + BOOST_REQUIRE(query.initialize(test::genesis)); + BOOST_REQUIRE_EQUAL(query.set_link(block1, database::context{}), 1u); + BOOST_REQUIRE_EQUAL(query.set_link(block2, database::context{}), 2u); + BOOST_REQUIRE_EQUAL(query.set_link(block3, database::context{}), 3u); + BOOST_REQUIRE(!query.is_malleable(0)); + BOOST_REQUIRE(query.is_malleable(1)); + BOOST_REQUIRE(query.is_malleable(2)); + BOOST_REQUIRE(query.is_malleable(3)); +} + +BOOST_AUTO_TEST_CASE(query_archive__is_malleable__non_malleable__false) +{ + settings settings{}; + settings.path = TEST_DIRECTORY; + test::chunk_store store{ settings }; + test::query_accessor query{ store }; + BOOST_REQUIRE_EQUAL(store.create(events), error::success); + BOOST_REQUIRE(query.initialize(test::genesis)); + BOOST_REQUIRE(query.set(test::block1a, context{})); + BOOST_REQUIRE(query.set(test::block2a, context{})); + BOOST_REQUIRE(!query.is_malleable(1)); + BOOST_REQUIRE(!query.is_malleable(2)); + BOOST_REQUIRE(!query.is_malleable(3)); + BOOST_REQUIRE(!query.is_malleable(4)); + BOOST_REQUIRE(!query.is_malleable(5)); + BOOST_REQUIRE(!query.is_malleable(42)); +} + BOOST_AUTO_TEST_CASE(query_archive__get_header__invalid_parent__expected) { constexpr auto root = system::base16_array("119192939495969798999a9b9c9d9e9f229192939495969798999a9b9c9d9e9f"); diff --git a/test/tables/archives/txs.cpp b/test/tables/archives/txs.cpp index 51f4bd62..83215d46 100644 --- a/test/tables/archives/txs.cpp +++ b/test/tables/archives/txs.cpp @@ -27,7 +27,7 @@ const table::txs::slab expected0{}; const table::txs::slab expected1 { {}, // schema::txs [all const static members] - false, + true, std_vector { 0x56341211_u32 @@ -46,7 +46,7 @@ const table::txs::slab expected2 const table::txs::slab expected3 { {}, // schema::txs [all const static members] - false, + true, std_vector { 0x56341231_u32, @@ -63,7 +63,10 @@ const data_chunk expected_file 0x11, 0x22, 0x33, // slab0 (count) [0] - 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, + + // slab0 (malleable) [false] + 0x00, // slab0 (txs) @@ -76,7 +79,10 @@ const data_chunk expected_file 0x11, 0x22, 0x33, // slab1 (count) [1] - 0x01, 0x00, 0x00, 0x00, + 0x01, 0x00, 0x00, + + // slab0 (malleable) [true] + 0x01, // slab1 (txs) 0x11, 0x12, 0x34, 0x56, @@ -90,7 +96,10 @@ const data_chunk expected_file 0x11, 0x22, 0x33, // slab2 (count) [2] - 0x02, 0x00, 0x00, 0x00, + 0x02, 0x00, 0x00, + + // slab0 (malleable) [false] + 0x00, // slab2 0x21, 0x12, 0x34, 0x56, @@ -105,7 +114,10 @@ const data_chunk expected_file 0x11, 0x22, 0x33, // slab3 (count) [3] - 0x03, 0x00, 0x00, 0x00, + 0x03, 0x00, 0x00, + + // slab0 (malleable) [true] + 0x01, // slab3 0x31, 0x12, 0x34, 0x56,