diff --git a/include/bitcoin/system/chain/views/transaction_view.hpp b/include/bitcoin/system/chain/views/transaction_view.hpp index daca4a7706..de5088ef29 100644 --- a/include/bitcoin/system/chain/views/transaction_view.hpp +++ b/include/bitcoin/system/chain/views/transaction_view.hpp @@ -39,6 +39,11 @@ class BC_API transaction_view final transaction_view(reader& source, const data_chunk& block_buffer, bool coinbase, bool witness) NOEXCEPT; + /// Serialization. + data_chunk to_data(bool witness) const NOEXCEPT; + void to_data(std::ostream& stream, bool witness) const NOEXCEPT; + void to_data(writer& sink, bool witness) const NOEXCEPT; + /// Properties. bool is_valid() const NOEXCEPT; bool is_coinbase() const NOEXCEPT; diff --git a/src/chain/views/block_view.cpp b/src/chain/views/block_view.cpp index bab4dfdcbb..c5dd89e51d 100644 --- a/src/chain/views/block_view.cpp +++ b/src/chain/views/block_view.cpp @@ -76,22 +76,20 @@ void block_view::to_data(std::ostream& stream, bool witness) const NOEXCEPT to_data(out, witness); } -void block_view::to_data(writer& , bool ) const NOEXCEPT +void block_view::to_data(writer& sink, bool witness) const NOEXCEPT { - ////if (witness) - ////{ - //// sink.write_bytes(*buffer_); - //// return; - ////} - //// - // TODO: write header from first bytes in buffer. - ////header_->to_data(sink); - ////sink.write_variable(txs_->size()); - //// - // TODO: add writers to tx, skip witness as applicable. - ////for (const auto& tx: *txs_) - //// tx->to_data(sink, witness); - BC_ASSERT_MSG(false, "not implemented"); + // The witnessed form is the original buffer. + if (witness) + { + sink.write_bytes(*buffer_); + return; + } + + // Strip witness: the header and transaction count are unaffected. + sink.write_bytes(buffer_->data(), header::serialized_size()); + sink.write_variable(txs_.size()); + for (const auto& tx: txs_) + tx.to_data(sink, false); } // public diff --git a/src/chain/views/transaction_view.cpp b/src/chain/views/transaction_view.cpp index ed7752f880..0d127373ea 100644 --- a/src/chain/views/transaction_view.cpp +++ b/src/chain/views/transaction_view.cpp @@ -18,6 +18,7 @@ */ #include +#include #include #include #include @@ -189,6 +190,43 @@ size_t transaction_view::serialized_size(bool witness) const NOEXCEPT return witness && is_segregated() ? size_ : stripped_size(); } +data_chunk transaction_view::to_data(bool witness) const NOEXCEPT +{ + data_chunk data(serialized_size(witness)); + stream::out::fast ostream(data); + write::bytes::fast out(ostream); + to_data(out, witness); + return data; +} + +void transaction_view::to_data(std::ostream& stream, + bool witness) const NOEXCEPT +{ + write::bytes::ostream out(stream); + to_data(out, witness); +} + +void transaction_view::to_data(writer& sink, bool witness) const NOEXCEPT +{ + // Witness can be stripped but never added (mirrors chain::transaction). + if (witness && is_segregated()) + { + sink.write_bytes(tx_ptr_, size_); + return; + } + + // Stripped form drops the witness marker/flag sentinels (after version) + // and the witness data (before locktime); inputs/outputs are unchanged. + const auto sentinels = is_zero(witnesses_size_) ? zero : sentinels_size; + const auto body = std::next(tx_ptr_, version_size + sentinels); + const auto body_size = size_ - witnesses_size_ - locktime_size - + version_size - sentinels; + + sink.write_bytes(tx_ptr_, version_size); + sink.write_bytes(body, body_size); + sink.write_bytes(std::next(tx_ptr_, size_ - locktime_size), locktime_size); +} + const hash_digest& transaction_view::hash(bool witness) const NOEXCEPT { return witness && is_segregated() ? wtxid_ : txid_; diff --git a/test/chain/views/block_view.cpp b/test/chain/views/block_view.cpp index 149d48cf23..103dfa5706 100644 --- a/test/chain/views/block_view.cpp +++ b/test/chain/views/block_view.cpp @@ -68,6 +68,56 @@ BOOST_AUTO_TEST_CASE(block_view__construct__block1a_non_witness__valid) BOOST_CHECK_EQUAL(view.serialized_size(false), block.serialized_size(false)); } +// to_data + +BOOST_AUTO_TEST_CASE(block_view__to_data__genesis__matches_block) +{ + const auto& block = test::genesis; + const chain::block_view view{ block.to_data(true), true }; + BOOST_CHECK_EQUAL(view.to_data(true), block.to_data(true)); + BOOST_CHECK_EQUAL(view.to_data(false), block.to_data(false)); +} + +BOOST_AUTO_TEST_CASE(block_view__to_data__block1a_witness__matches_block) +{ + const auto& block = test::block1a; + const chain::block_view view{ block.to_data(true), true }; + BOOST_CHECK_EQUAL(view.to_data(true), block.to_data(true)); + BOOST_CHECK_EQUAL(view.to_data(false), block.to_data(false)); +} + +BOOST_AUTO_TEST_CASE(block_view__to_data__block1a_stripped_source__stripped) +{ + const auto& block = test::block1a; + const chain::block_view view{ block.to_data(false), false }; + BOOST_CHECK_EQUAL(view.to_data(true), block.to_data(false)); + BOOST_CHECK_EQUAL(view.to_data(false), block.to_data(false)); +} + +BOOST_AUTO_TEST_CASE(block_view__to_data__block2a_witness__matches_block) +{ + // block2a is a multi-transaction block bearing witness data. + const auto& block = test::block2a; + const chain::block_view view{ block.to_data(true), true }; + BOOST_REQUIRE(view.is_segregated()); + BOOST_CHECK_EQUAL(view.transactions(), block.transactions_ptr()->size()); + BOOST_CHECK_EQUAL(view.to_data(true), block.to_data(true)); + BOOST_CHECK_EQUAL(view.to_data(false), block.to_data(false)); +} + +BOOST_AUTO_TEST_CASE(block_view__to_data__mixed_witness_and_legacy__matches_block) +{ + // block2c has one segregated (witness) and one legacy (non-witness) + // transaction, so the per-transaction witness strip is exercised both + // ways within one to_data(false) call. + const auto& block = test::block2c; + const chain::block_view view{ block.to_data(true), true }; + BOOST_REQUIRE(view.is_segregated()); + BOOST_REQUIRE_EQUAL(view.transactions(), 2u); + BOOST_CHECK_EQUAL(view.to_data(true), block.to_data(true)); + BOOST_CHECK_EQUAL(view.to_data(false), block.to_data(false)); +} + // identify1 and identify2 BOOST_AUTO_TEST_CASE(block_view__identify__genesis__expected) diff --git a/test/chain/views/transaction_view.cpp b/test/chain/views/transaction_view.cpp index 60e5c0aa2b..1a6518903a 100644 --- a/test/chain/views/transaction_view.cpp +++ b/test/chain/views/transaction_view.cpp @@ -273,4 +273,86 @@ BOOST_AUTO_TEST_CASE(transaction_view__write_witness__tx4_witness__expected) BOOST_CHECK_EQUAL(witness, expected); } +// to_data + +BOOST_AUTO_TEST_CASE(transaction_view__to_data__tx4_witness__matches_transaction) +{ + // Serialized to buffer WITH witness data, parsed for a witness node. + const auto transaction = test::tx4.to_data(true); + const auto& tx = test::tx4; + stream::in::fast istream{ transaction }; + read::bytes::fast reader{ istream }; + + const chain::transaction_view view{ reader, transaction, false, true }; + BOOST_REQUIRE(view.is_valid()); + BOOST_REQUIRE(view.is_segregated()); + + // Witnessed form is a copy; stripped drops marker, flag and witness. + data_chunk witnessed(view.serialized_size(true)); + stream::out::fast witnessed_stream(witnessed); + write::bytes::fast witnessed_sink(witnessed_stream); + view.to_data(witnessed_sink, true); + BOOST_CHECK_EQUAL(witnessed, tx.to_data(true)); + + data_chunk stripped(view.serialized_size(false)); + stream::out::fast stripped_stream(stripped); + write::bytes::fast stripped_sink(stripped_stream); + view.to_data(stripped_sink, false); + BOOST_CHECK_EQUAL(stripped, tx.to_data(false)); +} + +BOOST_AUTO_TEST_CASE(transaction_view__to_data__tx4_stripped_source__stripped) +{ + // Serialized to buffer WITHOUT witness data (nothing to strip). + const auto transaction = test::tx4.to_data(false); + const auto& tx = test::tx4; + stream::in::fast istream{ transaction }; + read::bytes::fast reader{ istream }; + + const chain::transaction_view view{ reader, transaction, false, false }; + BOOST_REQUIRE(view.is_valid()); + BOOST_REQUIRE(!view.is_segregated()); + + // Both forms are the stripped bytes; witness is never added. + data_chunk witnessed(view.serialized_size(true)); + stream::out::fast witnessed_stream(witnessed); + write::bytes::fast witnessed_sink(witnessed_stream); + view.to_data(witnessed_sink, true); + BOOST_CHECK_EQUAL(witnessed, tx.to_data(false)); + + data_chunk stripped(view.serialized_size(false)); + stream::out::fast stripped_stream(stripped); + write::bytes::fast stripped_sink(stripped_stream); + view.to_data(stripped_sink, false); + BOOST_CHECK_EQUAL(stripped, tx.to_data(false)); +} + +BOOST_AUTO_TEST_CASE(transaction_view__to_data__tx4_overloads__match_transaction) +{ + // Serialized to buffer WITH witness data, parsed for a witness node. + const auto transaction = test::tx4.to_data(true); + const auto& tx = test::tx4; + stream::in::fast istream{ transaction }; + read::bytes::fast reader{ istream }; + + const chain::transaction_view view{ reader, transaction, false, true }; + BOOST_REQUIRE(view.is_valid()); + BOOST_REQUIRE(view.is_segregated()); + + // Chunk overload. + BOOST_CHECK_EQUAL(view.to_data(true), tx.to_data(true)); + BOOST_CHECK_EQUAL(view.to_data(false), tx.to_data(false)); + + // Stream overload. + std::stringstream witnessed_stream{}; + view.to_data(witnessed_stream, true); + BOOST_REQUIRE(witnessed_stream); + BOOST_CHECK_EQUAL(to_chunk(witnessed_stream.str()), tx.to_data(true)); + + std::stringstream stripped_stream{}; + view.to_data(stripped_stream, false); + BOOST_REQUIRE(stripped_stream); + BOOST_CHECK_EQUAL(to_chunk(stripped_stream.str()), tx.to_data(false)); +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/test/mocks/blocks.cpp b/test/mocks/blocks.cpp index e6c1788c7b..1b20fc26ec 100644 --- a/test/mocks/blocks.cpp +++ b/test/mocks/blocks.cpp @@ -730,6 +730,53 @@ const block block2b } } }; +const block block2c +{ + header + { + 0x31323334, // version + block1a.hash(), // previous_block_hash + hash_digest{ 0x2c },// merkle_root + 0x41424344, // timestamp + 0x51525354, // bits + 0x61626364 // nonce + }, + transactions + { + transaction // segregated (has witness) + { + 0xb2, // version + inputs + { + input + { + point{ block1a.transactions_ptr()->front()->hash(false), 0x00 }, + script{ { { opcode::checkmultisig }, { opcode::pick } } }, + witness{ "[242424]" }, + 0xb2 // sequence + } + }, + outputs{ output{ 0x81, script{ { { opcode::pick } } } } }, + 0x81 // locktime + }, + transaction // legacy (no witness) + { + 0xb3, // version + inputs + { + input + { + point{ block1a.transactions_ptr()->front()->hash(false), 0x01 }, + script{ { { opcode::checkmultisig }, { opcode::roll } } }, + witness{}, + 0x83 // sequence + } + }, + outputs{ output{ 0x81, script{ { { opcode::pick } } } } }, + 0x81 // locktime + } + } +}; const transaction tx2b { transaction diff --git a/test/mocks/blocks.hpp b/test/mocks/blocks.hpp index 9ea23b4aba..4d98b48eae 100644 --- a/test/mocks/blocks.hpp +++ b/test/mocks/blocks.hpp @@ -92,6 +92,7 @@ extern const system::chain::transaction tx4; extern const system::chain::transaction tx5; extern const system::chain::block block1b; extern const system::chain::block block2b; +extern const system::chain::block block2c; extern const system::chain::transaction tx2b; extern const system::chain::block mock_block_b; extern const system::chain::block mock_block_c;