Skip to content
This repository has been archived by the owner on Aug 2, 2022. It is now read-only.

Explicit ABI conversion of signed_transaction - merge 2.1.x #9989

122 changes: 113 additions & 9 deletions libraries/chain/include/eosio/chain/abi_serializer.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -557,17 +557,9 @@ namespace impl {
out(name, std::move(mvo));
}

/**
* overload of to_variant_object for transaction
*
* This matches the FC_REFLECT for this type, but this is provided to allow extracting the contents of trx.transaction_extensions
*/
template<typename Resolver>
static void add( mutable_variant_object &out, const char* name, const transaction& trx, Resolver resolver, abi_traverse_context& ctx )
static void add_transaction( mutable_variant_object& mvo, const transaction& trx, Resolver resolver, abi_traverse_context& ctx )
{
static_assert(fc::reflector<transaction>::total_member_count == 9);
auto h = ctx.enter_scope();
mutable_variant_object mvo;
mvo("expiration", trx.expiration);
mvo("ref_block_num", trx.ref_block_num);
mvo("ref_block_prefix", trx.ref_block_prefix);
Expand All @@ -583,6 +575,38 @@ namespace impl {
const auto& deferred_transaction_generation = std::get<deferred_transaction_generation_context>(exts.lower_bound(deferred_transaction_generation_context::extension_id())->second);
mvo("deferred_transaction_generation", deferred_transaction_generation);
}
}

/**
* overload of to_variant_object for transaction
*
* This matches the FC_REFLECT for this type, but this is provided to allow extracting the contents of trx.transaction_extensions
*/
template<typename Resolver>
static void add( mutable_variant_object &out, const char* name, const transaction& trx, Resolver resolver, abi_traverse_context& ctx )
{
static_assert(fc::reflector<transaction>::total_member_count == 9);
auto h = ctx.enter_scope();
mutable_variant_object mvo;
add_transaction(mvo, trx, resolver, ctx);

out(name, std::move(mvo));
}

/**
* overload of to_variant_object for signed_transaction
*
* This matches the FC_REFLECT for this type, but this is provided to allow extracting the contents of trx.transaction_extensions
*/
template<typename Resolver>
static void add( mutable_variant_object &out, const char* name, const signed_transaction& trx, Resolver resolver, abi_traverse_context& ctx )
{
static_assert(fc::reflector<signed_transaction>::total_member_count == 11);
auto h = ctx.enter_scope();
mutable_variant_object mvo;
add_transaction(mvo, trx, resolver, ctx);
mvo("signatures", trx.signatures);
mvo("context_free_data", trx.context_free_data);

out(name, std::move(mvo));
}
Expand Down Expand Up @@ -818,6 +842,86 @@ namespace impl {
"Failed to deserialize data for ${account}:${name}", ("account", act.account)("name", act.name));
}

template<typename Resolver>
static void extract_transaction( const variant_object& vo, transaction& trx, Resolver resolver, abi_traverse_context& ctx )
{
if (vo.contains("expiration")) {
from_variant(vo["expiration"], trx.expiration);
}
if (vo.contains("ref_block_num")) {
from_variant(vo["ref_block_num"], trx.ref_block_num);
}
if (vo.contains("ref_block_prefix")) {
from_variant(vo["ref_block_prefix"], trx.ref_block_prefix);
}
if (vo.contains("max_net_usage_words")) {
from_variant(vo["max_net_usage_words"], trx.max_net_usage_words);
}
if (vo.contains("max_cpu_usage_ms")) {
from_variant(vo["max_cpu_usage_ms"], trx.max_cpu_usage_ms);
}
if (vo.contains("delay_sec")) {
from_variant(vo["delay_sec"], trx.delay_sec);
}
if (vo.contains("context_free_actions")) {
extract(vo["context_free_actions"], trx.context_free_actions, resolver, ctx);
}
if (vo.contains("actions")) {
extract(vo["actions"], trx.actions, resolver, ctx);
}

// can have "deferred_transaction_generation" (if there is a deferred transaction and the extension was "extracted" to show data),
// or "transaction_extensions" (either as empty or containing the packed deferred transaction),
// or both (when there is a deferred transaction and extension was "extracted" to show data and a redundant "transaction_extensions" was provided),
// or neither (only if extension was "extracted" and there was no deferred transaction to extract)
if (vo.contains("deferred_transaction_generation")) {
std::cerr << "has deferred_transaction_generation\n";
deferred_transaction_generation_context deferred_transaction_generation;
from_variant(vo["deferred_transaction_generation"], deferred_transaction_generation);
emplace_extension(
trx.transaction_extensions,
deferred_transaction_generation_context::extension_id(),
fc::raw::pack( deferred_transaction_generation )
);
// if both are present, they need to match
if (vo.contains("transaction_extensions")) {
std::cerr << "also has transaction_extensions\n";
extensions_type trx_extensions;
from_variant(vo["transaction_extensions"], trx_extensions);
EOS_ASSERT(trx.transaction_extensions == trx_extensions, packed_transaction_type_exception,
"Transaction contained deferred_transaction_generation and transaction_extensions that did not match");
}
}
else if (vo.contains("transaction_extensions")) {
std::cerr << "has transaction_extensions\n";
from_variant(vo["transaction_extensions"], trx.transaction_extensions);
}
}

template<typename Resolver>
static void extract( const fc::variant& v, transaction& trx, Resolver resolver, abi_traverse_context& ctx )
{
static_assert(fc::reflector<transaction>::total_member_count == 9);
auto h = ctx.enter_scope();
const variant_object& vo = v.get_object();
extract_transaction(vo, trx, resolver, ctx);
}

template<typename Resolver>
static void extract( const fc::variant& v, signed_transaction& strx, Resolver resolver, abi_traverse_context& ctx )
{
static_assert(fc::reflector<signed_transaction>::total_member_count == 11);
auto h = ctx.enter_scope();
const variant_object& vo = v.get_object();
extract_transaction(vo, strx, resolver, ctx);
if (vo.contains("signatures")) {
from_variant(vo["signatures"], strx.signatures);
}
if (vo.contains("context_free_data")) {
from_variant(vo["context_free_data"], strx.context_free_data);
}
}

template<typename Resolver>
static void extract( const fc::variant& v, packed_transaction_v0& ptrx, Resolver resolver, abi_traverse_context& ctx )
{
Expand Down
188 changes: 188 additions & 0 deletions unittests/abi_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3482,4 +3482,192 @@ BOOST_AUTO_TEST_CASE(abi_to_variant__add_action__no_return_value)
BOOST_CHECK_EQUAL(res, expected_json);
}

namespace {
template<typename Transaction>
Transaction populate() {
Transaction txn;
txn.ref_block_num = 1;
txn.ref_block_prefix = 2;
txn.expiration.from_iso_string("2021-12-20T15:30");
name a = "alice"_n;
txn.context_free_actions.emplace_back(
vector<permission_level>{{"testapi1"_n, config::active_name}},
newaccount{
.creator = config::system_account_name,
.name = a,
.owner = authority( get_public_key( a, "owner" )),
.active = authority( get_public_key( a, "active" ) )
});
txn.context_free_actions.emplace_back(
vector<permission_level>{{"testapi2"_n, config::active_name}},
action1{ 15, 23, (uint8_t)3});
txn.actions.emplace_back(
vector<permission_level>{{"testapi3"_n, config::active_name}},
action2{ 42, 67, (uint8_t)1});
txn.actions.emplace_back(
vector<permission_level>{{"testapi4"_n, config::active_name}},
action2{ 61, 23, (uint8_t)2});
txn.max_net_usage_words = 15;
txn.max_cpu_usage_ms = 43;
deferred_transaction_generation_context dtg;
dtg.sender_trx_id = txn.id(); // just populating it with a valid transaction id
uint64_t upper = 0x0123456789abcdef;
uint64_t lower = 0x02468ace13579bdf;
dtg.sender_id = upper;
dtg.sender_id <<= 64;
dtg.sender_id |= lower;
dtg.sender = "test.account"_n;
emplace_extension(
txn.transaction_extensions,
deferred_transaction_generation_context::extension_id(),
fc::raw::pack( dtg )
);
return txn;
}

}

BOOST_AUTO_TEST_CASE(transaction_extensions_tests)
{
auto txn = populate<chain::transaction>();

// create a variant from txn
mutable_variant_object mvo;
eosio::chain::impl::abi_traverse_context ctx(abi_serializer::create_yield_function(max_serialization_time));
auto abi = eosio_contract_abi(fc::json::from_string(my_abi).as<abi_def>());
eosio::chain::impl::abi_to_variant::add(mvo, "test", txn, get_resolver(abi), ctx);
const std::string mvo_as_string = fc::json::to_string(mvo, fc::time_point::now() + max_serialization_time);
// since this ends up using abi_serializer::add, we will have deferred_transaction_generation
BOOST_REQUIRE(mvo_as_string.find("deferred_transaction_generation") != string::npos);
BOOST_REQUIRE(mvo_as_string.find("transaction_extensions") == string::npos);
// create a clone of the original txn
chain::transaction txn_clone;
abi_serializer::from_variant(mvo["test"], txn_clone, get_resolver(), abi_serializer::create_yield_function( max_serialization_time ));
BOOST_REQUIRE_EQUAL(txn_clone.transaction_extensions.size(), 1);
BOOST_REQUIRE(txn_clone.transaction_extensions == txn.transaction_extensions);
mutable_variant_object mvo2;
eosio::chain::impl::abi_to_variant::add(mvo2, "test", txn_clone, get_resolver(abi), ctx);
const std::string mvo2_as_string = fc::json::to_string(mvo2, fc::time_point::now() + max_serialization_time);
// verify that the variant created from the original txn and the variant from the clone match
BOOST_REQUIRE_EQUAL(mvo_as_string, mvo2_as_string);

// create variant directly
fc::variant direct_var(txn);
const std::string direct_var_as_string = fc::json::to_string(direct_var, fc::time_point::now() + max_serialization_time);
// since this ends up using FC_REFLECT, we will have transaction_extensions
BOOST_REQUIRE(direct_var_as_string.find("deferred_transaction_generation") == string::npos);
BOOST_REQUIRE(direct_var_as_string.find("transaction_extensions") != string::npos);
const auto trans_ext = direct_var["transaction_extensions"].get_array();
chain::transaction txn_clone2;
abi_serializer::from_variant(direct_var, txn_clone2, get_resolver(), abi_serializer::create_yield_function( max_serialization_time ));
mutable_variant_object mvo3;
eosio::chain::impl::abi_to_variant::add(mvo3, "test", txn_clone2, get_resolver(abi), ctx);
const std::string mvo3_as_string = fc::json::to_string(mvo3, fc::time_point::now() + max_serialization_time);
BOOST_REQUIRE_EQUAL(mvo_as_string, mvo3_as_string);

// add "transaction_extensions" to mvo, so it contains redundant (but identical) extensions data
mutable_variant_object mvo_txn(mvo["test"]);
mvo_txn["transaction_extensions"] = trans_ext;
mvo["test"] = mvo_txn;
const std::string redundant_ext_as_string = fc::json::to_string(mvo, fc::time_point::now() + max_serialization_time);
// verifying both are present
BOOST_REQUIRE(redundant_ext_as_string.find("deferred_transaction_generation") != string::npos);
BOOST_REQUIRE(redundant_ext_as_string.find("transaction_extensions") != string::npos);
chain::transaction txn_clone3;
abi_serializer::from_variant(mvo["test"], txn_clone3, get_resolver(), abi_serializer::create_yield_function( max_serialization_time ));
BOOST_REQUIRE_EQUAL(txn_clone3.transaction_extensions.size(), 1);

// empty the transaction_extensions, so that the data is not consistent
auto txn_no_ext = populate<chain::transaction>();
txn_no_ext.transaction_extensions.clear();
fc::variant direct_txn_no_ext(txn_no_ext);
const auto trans_ext_empty = direct_txn_no_ext["transaction_extensions"].get_array();
mvo_txn["transaction_extensions"] = trans_ext_empty;
mvo["test"] = mvo_txn;
const std::string incompatible_ext_as_string = fc::json::to_string(mvo, fc::time_point::now() + max_serialization_time);
std::cerr << incompatible_ext_as_string << "\n";
// verifying both are present
BOOST_REQUIRE(incompatible_ext_as_string.find("deferred_transaction_generation") != string::npos);
BOOST_REQUIRE(incompatible_ext_as_string.find("transaction_extensions") != string::npos);
chain::transaction txn_clone4;
using eosio::testing::fc_exception_message_starts_with;
BOOST_CHECK_EXCEPTION( abi_serializer::from_variant(mvo["test"], txn_clone4, get_resolver(), abi_serializer::create_yield_function( max_serialization_time )),
packed_transaction_type_exception,
fc_exception_message_starts_with("Transaction contained deferred_transaction_generation and transaction_extensions that did not match") );
}

BOOST_AUTO_TEST_CASE(signed_transaction_extensions_tests)
{
auto txn = populate<chain::signed_transaction>();
signature_type empty_sig;
txn.signatures.push_back(empty_sig);
txn.signatures.push_back(empty_sig);
txn.context_free_data.push_back(bytes{ 10, 'a' });
txn.context_free_data.push_back(bytes{ 20, 'b' });

// create a variant from txn
mutable_variant_object mvo;
eosio::chain::impl::abi_traverse_context ctx(abi_serializer::create_yield_function(max_serialization_time));
auto abi = eosio_contract_abi(fc::json::from_string(my_abi).as<abi_def>());
eosio::chain::impl::abi_to_variant::add(mvo, "test", txn, get_resolver(abi), ctx);
const std::string mvo_as_string = fc::json::to_string(mvo, fc::time_point::now() + max_serialization_time);
// since this ends up using abi_serializer::add, we will have deferred_transaction_generation
BOOST_REQUIRE(mvo_as_string.find("deferred_transaction_generation") != string::npos);
BOOST_REQUIRE(mvo_as_string.find("transaction_extensions") == string::npos);
// create a clone of the original txn
chain::signed_transaction txn_clone;
abi_serializer::from_variant(mvo["test"], txn_clone, get_resolver(), abi_serializer::create_yield_function( max_serialization_time ));
BOOST_REQUIRE_EQUAL(txn_clone.transaction_extensions.size(), 1);
BOOST_REQUIRE(txn_clone.transaction_extensions == txn.transaction_extensions);
mutable_variant_object mvo2;
eosio::chain::impl::abi_to_variant::add(mvo2, "test", txn_clone, get_resolver(abi), ctx);
const std::string mvo2_as_string = fc::json::to_string(mvo2, fc::time_point::now() + max_serialization_time);
// verify that the variant created from the original txn and the variant from the clone match
BOOST_REQUIRE_EQUAL(mvo_as_string, mvo2_as_string);

// create variant directly
fc::variant direct_var(txn);
const std::string direct_var_as_string = fc::json::to_string(direct_var, fc::time_point::now() + max_serialization_time);
// since this ends up using FC_REFLECT, we will have transaction_extensions
BOOST_REQUIRE(direct_var_as_string.find("deferred_transaction_generation") == string::npos);
BOOST_REQUIRE(direct_var_as_string.find("transaction_extensions") != string::npos);
const auto trans_ext = direct_var["transaction_extensions"].get_array();
chain::signed_transaction txn_clone2;
abi_serializer::from_variant(direct_var, txn_clone2, get_resolver(), abi_serializer::create_yield_function( max_serialization_time ));
mutable_variant_object mvo3;
eosio::chain::impl::abi_to_variant::add(mvo3, "test", txn_clone2, get_resolver(abi), ctx);
const std::string mvo3_as_string = fc::json::to_string(mvo3, fc::time_point::now() + max_serialization_time);
BOOST_REQUIRE_EQUAL(mvo_as_string, mvo3_as_string);

// add "transaction_extensions" to mvo, so it contains redundant (but identical) extensions data
mutable_variant_object mvo_txn(mvo["test"]);
mvo_txn["transaction_extensions"] = trans_ext;
mvo["test"] = mvo_txn;
const std::string redundant_ext_as_string = fc::json::to_string(mvo, fc::time_point::now() + max_serialization_time);
// verifying both are present
BOOST_REQUIRE(redundant_ext_as_string.find("deferred_transaction_generation") != string::npos);
BOOST_REQUIRE(redundant_ext_as_string.find("transaction_extensions") != string::npos);
chain::signed_transaction txn_clone3;
abi_serializer::from_variant(mvo["test"], txn_clone3, get_resolver(), abi_serializer::create_yield_function( max_serialization_time ));
BOOST_REQUIRE_EQUAL(txn_clone3.transaction_extensions.size(), 1);

// empty the transaction_extensions, so that the data is not consistent
auto txn_no_ext = populate<chain::signed_transaction>();
txn_no_ext.transaction_extensions.clear();
fc::variant direct_txn_no_ext(txn_no_ext);
const auto trans_ext_empty = direct_txn_no_ext["transaction_extensions"].get_array();
mvo_txn["transaction_extensions"] = trans_ext_empty;
mvo["test"] = mvo_txn;
const std::string incompatible_ext_as_string = fc::json::to_string(mvo, fc::time_point::now() + max_serialization_time);
std::cerr << incompatible_ext_as_string << "\n";
// verifying both are present
BOOST_REQUIRE(incompatible_ext_as_string.find("deferred_transaction_generation") != string::npos);
BOOST_REQUIRE(incompatible_ext_as_string.find("transaction_extensions") != string::npos);
chain::signed_transaction txn_clone4;
using eosio::testing::fc_exception_message_starts_with;
BOOST_CHECK_EXCEPTION( abi_serializer::from_variant(mvo["test"], txn_clone4, get_resolver(), abi_serializer::create_yield_function( max_serialization_time )),
packed_transaction_type_exception,
fc_exception_message_starts_with("Transaction contained deferred_transaction_generation and transaction_extensions that did not match") );
}

BOOST_AUTO_TEST_SUITE_END()