compilation and test case fixes
This commit is contained in:
parent
ef6cf00f60
commit
df4d8665a2
5 changed files with 43 additions and 149 deletions
|
|
@ -1723,6 +1723,9 @@ set<public_key_type> database_api_impl::get_potential_signatures( const signed_t
|
||||||
|
|
||||||
set<address> database_api_impl::get_potential_address_signatures( const signed_transaction& trx )const
|
set<address> database_api_impl::get_potential_address_signatures( const signed_transaction& trx )const
|
||||||
{
|
{
|
||||||
|
auto chain_time = _db.head_block_time();
|
||||||
|
bool allow_non_immediate_owner = ( chain_time >= HARDFORK_1002_TIME );
|
||||||
|
|
||||||
set<address> result;
|
set<address> result;
|
||||||
trx.get_required_signatures(
|
trx.get_required_signatures(
|
||||||
_db.get_chain_id(),
|
_db.get_chain_id(),
|
||||||
|
|
@ -1741,6 +1744,7 @@ set<address> database_api_impl::get_potential_address_signatures( const signed_t
|
||||||
result.insert(k);
|
result.insert(k);
|
||||||
return &auth;
|
return &auth;
|
||||||
},
|
},
|
||||||
|
allow_non_immediate_owner,
|
||||||
_db.get_global_properties().parameters.max_authority_depth
|
_db.get_global_properties().parameters.max_authority_depth
|
||||||
);
|
);
|
||||||
return result;
|
return result;
|
||||||
|
|
|
||||||
|
|
@ -188,6 +188,9 @@ namespace graphene { namespace chain {
|
||||||
|
|
||||||
/// Removes all operations and signatures
|
/// Removes all operations and signatures
|
||||||
void clear() { operations.clear(); signatures.clear(); }
|
void clear() { operations.clear(); signatures.clear(); }
|
||||||
|
|
||||||
|
/** Removes all signatures */
|
||||||
|
void clear_signatures() { signatures.clear(); }
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -341,22 +341,23 @@ set<public_key_type> signed_transaction::get_required_signatures(
|
||||||
vector<authority> other;
|
vector<authority> other;
|
||||||
get_required_authorities( required_active, required_owner, other );
|
get_required_authorities( required_active, required_owner, other );
|
||||||
|
|
||||||
|
const flat_set<public_key_type>& signature_keys = get_signature_keys(chain_id);
|
||||||
sign_state s( get_signature_keys( chain_id ), get_active, get_owner, allow_non_immediate_owner, max_recursion_depth, available_keys );
|
sign_state s( signature_keys, get_active, get_owner, allow_non_immediate_owner, max_recursion_depth, available_keys );
|
||||||
|
|
||||||
for( const auto& auth : other )
|
for( const auto& auth : other )
|
||||||
s.check_authority(&auth);
|
s.check_authority(&auth);
|
||||||
for( auto& owner : required_owner )
|
for( auto& owner : required_owner )
|
||||||
s.check_authority( get_owner( owner ) );
|
s.check_authority( get_owner( owner ) );
|
||||||
for( auto& active : required_active )
|
for( auto& active : required_active )
|
||||||
s.check_authority( active );
|
s.check_authority( active ) || s.check_authority( get_owner( active ) );
|
||||||
|
|
||||||
s.remove_unused_signatures();
|
s.remove_unused_signatures();
|
||||||
|
|
||||||
set<public_key_type> result;
|
set<public_key_type> result;
|
||||||
|
|
||||||
for( auto& provided_sig : s.provided_signatures )
|
for( auto& provided_sig : s.provided_signatures )
|
||||||
if( available_keys.find( provided_sig.first ) != available_keys.end() )
|
if( available_keys.find( provided_sig.first ) != available_keys.end()
|
||||||
|
&& signature_keys.find( provided_sig.first ) == signature_keys.end() )
|
||||||
result.insert( provided_sig.first );
|
result.insert( provided_sig.first );
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|
|
||||||
|
|
@ -1398,7 +1398,7 @@ BOOST_FIXTURE_TEST_CASE( parent_owner_test, database_fixture )
|
||||||
{
|
{
|
||||||
//wdump( (tx)(available_keys) );
|
//wdump( (tx)(available_keys) );
|
||||||
set<public_key_type> result_set = tx.get_required_signatures(db.get_chain_id(), available_keys,
|
set<public_key_type> result_set = tx.get_required_signatures(db.get_chain_id(), available_keys,
|
||||||
get_active, get_owner, after_hf_584, false);
|
get_active, get_owner, after_hf_584);
|
||||||
//wdump( (result_set)(ref_set) );
|
//wdump( (result_set)(ref_set) );
|
||||||
return result_set == ref_set;
|
return result_set == ref_set;
|
||||||
} ;
|
} ;
|
||||||
|
|
@ -1514,87 +1514,87 @@ BOOST_FIXTURE_TEST_CASE( parent_owner_test, database_fixture )
|
||||||
BOOST_CHECK( chk( tx, true, { gavin_active_pub, gavin_owner_pub }, { gavin_active_pub } ) );
|
BOOST_CHECK( chk( tx, true, { gavin_active_pub, gavin_owner_pub }, { gavin_active_pub } ) );
|
||||||
|
|
||||||
sign( tx, alice_owner_key );
|
sign( tx, alice_owner_key );
|
||||||
GRAPHENE_REQUIRE_THROW( tx.verify_authority( db.get_chain_id(), get_active, get_owner, false, false ), fc::exception );
|
GRAPHENE_REQUIRE_THROW( tx.verify_authority( db.get_chain_id(), get_active, get_owner, false ), fc::exception );
|
||||||
GRAPHENE_REQUIRE_THROW( PUSH_TX( db, tx, database::skip_transaction_dupe_check ), fc::exception );
|
GRAPHENE_REQUIRE_THROW( PUSH_TX( db, tx, database::skip_transaction_dupe_check ), fc::exception );
|
||||||
tx.verify_authority( db.get_chain_id(), get_active, get_owner, true, false );
|
tx.verify_authority( db.get_chain_id(), get_active, get_owner, true);
|
||||||
tx.clear_signatures();
|
tx.clear_signatures();
|
||||||
|
|
||||||
sign( tx, alice_active_key );
|
sign( tx, alice_active_key );
|
||||||
tx.verify_authority( db.get_chain_id(), get_active, get_owner, false, false );
|
tx.verify_authority( db.get_chain_id(), get_active, get_owner, false);
|
||||||
PUSH_TX( db, tx, database::skip_transaction_dupe_check );
|
PUSH_TX( db, tx, database::skip_transaction_dupe_check );
|
||||||
tx.verify_authority( db.get_chain_id(), get_active, get_owner, true, false );
|
tx.verify_authority( db.get_chain_id(), get_active, get_owner, true);
|
||||||
tx.clear_signatures();
|
tx.clear_signatures();
|
||||||
|
|
||||||
sign( tx, bob_owner_key );
|
sign( tx, bob_owner_key );
|
||||||
tx.verify_authority( db.get_chain_id(), get_active, get_owner, false, false );
|
tx.verify_authority( db.get_chain_id(), get_active, get_owner, false);
|
||||||
PUSH_TX( db, tx, database::skip_transaction_dupe_check );
|
PUSH_TX( db, tx, database::skip_transaction_dupe_check );
|
||||||
tx.verify_authority( db.get_chain_id(), get_active, get_owner, true, false );
|
tx.verify_authority( db.get_chain_id(), get_active, get_owner, true);
|
||||||
tx.clear_signatures();
|
tx.clear_signatures();
|
||||||
|
|
||||||
sign( tx, bob_active_key );
|
sign( tx, bob_active_key );
|
||||||
tx.verify_authority( db.get_chain_id(), get_active, get_owner, false, false );
|
tx.verify_authority( db.get_chain_id(), get_active, get_owner, false);
|
||||||
PUSH_TX( db, tx, database::skip_transaction_dupe_check );
|
PUSH_TX( db, tx, database::skip_transaction_dupe_check );
|
||||||
tx.verify_authority( db.get_chain_id(), get_active, get_owner, true, false );
|
tx.verify_authority( db.get_chain_id(), get_active, get_owner, true);
|
||||||
tx.clear_signatures();
|
tx.clear_signatures();
|
||||||
|
|
||||||
sign( tx, cindy_owner_key );
|
sign( tx, cindy_owner_key );
|
||||||
GRAPHENE_REQUIRE_THROW( tx.verify_authority( db.get_chain_id(), get_active, get_owner, false, false ), fc::exception );
|
GRAPHENE_REQUIRE_THROW( tx.verify_authority( db.get_chain_id(), get_active, get_owner, false), fc::exception );
|
||||||
GRAPHENE_REQUIRE_THROW( PUSH_TX( db, tx, database::skip_transaction_dupe_check ), fc::exception );
|
GRAPHENE_REQUIRE_THROW( PUSH_TX( db, tx, database::skip_transaction_dupe_check ), fc::exception );
|
||||||
tx.verify_authority( db.get_chain_id(), get_active, get_owner, true, false );
|
tx.verify_authority( db.get_chain_id(), get_active, get_owner, true);
|
||||||
tx.clear_signatures();
|
tx.clear_signatures();
|
||||||
|
|
||||||
sign( tx, cindy_active_key );
|
sign( tx, cindy_active_key );
|
||||||
GRAPHENE_REQUIRE_THROW( tx.verify_authority( db.get_chain_id(), get_active, get_owner, false, false ), fc::exception );
|
GRAPHENE_REQUIRE_THROW( tx.verify_authority( db.get_chain_id(), get_active, get_owner, false), fc::exception );
|
||||||
GRAPHENE_REQUIRE_THROW( PUSH_TX( db, tx, database::skip_transaction_dupe_check ), fc::exception );
|
GRAPHENE_REQUIRE_THROW( PUSH_TX( db, tx, database::skip_transaction_dupe_check ), fc::exception );
|
||||||
tx.verify_authority( db.get_chain_id(), get_active, get_owner, true, false );
|
tx.verify_authority( db.get_chain_id(), get_active, get_owner, true);
|
||||||
tx.clear_signatures();
|
tx.clear_signatures();
|
||||||
|
|
||||||
sign( tx, daisy_owner_key );
|
sign( tx, daisy_owner_key );
|
||||||
GRAPHENE_REQUIRE_THROW( tx.verify_authority( db.get_chain_id(), get_active, get_owner, false, false ), fc::exception );
|
GRAPHENE_REQUIRE_THROW( tx.verify_authority( db.get_chain_id(), get_active, get_owner, false), fc::exception );
|
||||||
GRAPHENE_REQUIRE_THROW( PUSH_TX( db, tx, database::skip_transaction_dupe_check ), fc::exception );
|
GRAPHENE_REQUIRE_THROW( PUSH_TX( db, tx, database::skip_transaction_dupe_check ), fc::exception );
|
||||||
tx.verify_authority( db.get_chain_id(), get_active, get_owner, true, false );
|
tx.verify_authority( db.get_chain_id(), get_active, get_owner, true);
|
||||||
tx.clear_signatures();
|
tx.clear_signatures();
|
||||||
|
|
||||||
sign( tx, daisy_active_key );
|
sign( tx, daisy_active_key );
|
||||||
tx.verify_authority( db.get_chain_id(), get_active, get_owner, false, false );
|
tx.verify_authority( db.get_chain_id(), get_active, get_owner, false);
|
||||||
PUSH_TX( db, tx, database::skip_transaction_dupe_check );
|
PUSH_TX( db, tx, database::skip_transaction_dupe_check );
|
||||||
tx.verify_authority( db.get_chain_id(), get_active, get_owner, true, false );
|
tx.verify_authority( db.get_chain_id(), get_active, get_owner, true);
|
||||||
tx.clear_signatures();
|
tx.clear_signatures();
|
||||||
|
|
||||||
sign( tx, edwin_owner_key );
|
sign( tx, edwin_owner_key );
|
||||||
GRAPHENE_REQUIRE_THROW( tx.verify_authority( db.get_chain_id(), get_active, get_owner, false, false ), fc::exception );
|
GRAPHENE_REQUIRE_THROW( tx.verify_authority( db.get_chain_id(), get_active, get_owner, false), fc::exception );
|
||||||
GRAPHENE_REQUIRE_THROW( PUSH_TX( db, tx, database::skip_transaction_dupe_check ), fc::exception );
|
GRAPHENE_REQUIRE_THROW( PUSH_TX( db, tx, database::skip_transaction_dupe_check ), fc::exception );
|
||||||
tx.verify_authority( db.get_chain_id(), get_active, get_owner, true, false );
|
tx.verify_authority( db.get_chain_id(), get_active, get_owner, true);
|
||||||
tx.clear_signatures();
|
tx.clear_signatures();
|
||||||
|
|
||||||
sign( tx, edwin_active_key );
|
sign( tx, edwin_active_key );
|
||||||
tx.verify_authority( db.get_chain_id(), get_active, get_owner, false, false );
|
tx.verify_authority( db.get_chain_id(), get_active, get_owner, false);
|
||||||
PUSH_TX( db, tx, database::skip_transaction_dupe_check );
|
PUSH_TX( db, tx, database::skip_transaction_dupe_check );
|
||||||
tx.verify_authority( db.get_chain_id(), get_active, get_owner, true, false );
|
tx.verify_authority( db.get_chain_id(), get_active, get_owner, true);
|
||||||
tx.clear_signatures();
|
tx.clear_signatures();
|
||||||
|
|
||||||
sign( tx, frank_owner_key );
|
sign( tx, frank_owner_key );
|
||||||
GRAPHENE_REQUIRE_THROW( tx.verify_authority( db.get_chain_id(), get_active, get_owner, false, false ), fc::exception );
|
GRAPHENE_REQUIRE_THROW( tx.verify_authority( db.get_chain_id(), get_active, get_owner, false), fc::exception );
|
||||||
GRAPHENE_REQUIRE_THROW( PUSH_TX( db, tx, database::skip_transaction_dupe_check ), fc::exception );
|
GRAPHENE_REQUIRE_THROW( PUSH_TX( db, tx, database::skip_transaction_dupe_check ), fc::exception );
|
||||||
tx.verify_authority( db.get_chain_id(), get_active, get_owner, true, false );
|
tx.verify_authority( db.get_chain_id(), get_active, get_owner, true);
|
||||||
tx.clear_signatures();
|
tx.clear_signatures();
|
||||||
|
|
||||||
sign( tx, frank_active_key );
|
sign( tx, frank_active_key );
|
||||||
GRAPHENE_REQUIRE_THROW( tx.verify_authority( db.get_chain_id(), get_active, get_owner, false, false ), fc::exception );
|
GRAPHENE_REQUIRE_THROW( tx.verify_authority( db.get_chain_id(), get_active, get_owner, false), fc::exception );
|
||||||
GRAPHENE_REQUIRE_THROW( PUSH_TX( db, tx, database::skip_transaction_dupe_check ), fc::exception );
|
GRAPHENE_REQUIRE_THROW( PUSH_TX( db, tx, database::skip_transaction_dupe_check ), fc::exception );
|
||||||
tx.verify_authority( db.get_chain_id(), get_active, get_owner, true, false );
|
tx.verify_authority( db.get_chain_id(), get_active, get_owner, true);
|
||||||
tx.clear_signatures();
|
tx.clear_signatures();
|
||||||
|
|
||||||
sign( tx, gavin_owner_key );
|
sign( tx, gavin_owner_key );
|
||||||
GRAPHENE_REQUIRE_THROW( tx.verify_authority( db.get_chain_id(), get_active, get_owner, false, false ), fc::exception );
|
GRAPHENE_REQUIRE_THROW( tx.verify_authority( db.get_chain_id(), get_active, get_owner, false), fc::exception );
|
||||||
GRAPHENE_REQUIRE_THROW( PUSH_TX( db, tx, database::skip_transaction_dupe_check ), fc::exception );
|
GRAPHENE_REQUIRE_THROW( PUSH_TX( db, tx, database::skip_transaction_dupe_check ), fc::exception );
|
||||||
tx.verify_authority( db.get_chain_id(), get_active, get_owner, true, false );
|
tx.verify_authority( db.get_chain_id(), get_active, get_owner, true);
|
||||||
tx.clear_signatures();
|
tx.clear_signatures();
|
||||||
|
|
||||||
sign( tx, gavin_active_key );
|
sign( tx, gavin_active_key );
|
||||||
tx.verify_authority( db.get_chain_id(), get_active, get_owner, false, false );
|
tx.verify_authority( db.get_chain_id(), get_active, get_owner, false);
|
||||||
PUSH_TX( db, tx, database::skip_transaction_dupe_check );
|
PUSH_TX( db, tx, database::skip_transaction_dupe_check );
|
||||||
tx.verify_authority( db.get_chain_id(), get_active, get_owner, true, false );
|
tx.verify_authority( db.get_chain_id(), get_active, get_owner, true);
|
||||||
tx.clear_signatures();
|
tx.clear_signatures();
|
||||||
|
|
||||||
// proposal tests
|
// proposal tests
|
||||||
|
|
@ -1823,120 +1823,4 @@ BOOST_FIXTURE_TEST_CASE( parent_owner_test, database_fixture )
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This test case reproduces https://github.com/bitshares/bitshares-core/issues/944
|
|
||||||
/// and https://github.com/bitshares/bitshares-core/issues/580
|
|
||||||
BOOST_FIXTURE_TEST_CASE( missing_owner_auth_test, database_fixture )
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
ACTORS(
|
|
||||||
(alice)
|
|
||||||
);
|
|
||||||
|
|
||||||
auto set_auth = [&](
|
|
||||||
account_id_type aid,
|
|
||||||
const authority& active,
|
|
||||||
const authority& owner
|
|
||||||
)
|
|
||||||
{
|
|
||||||
signed_transaction tx;
|
|
||||||
account_update_operation op;
|
|
||||||
op.account = aid;
|
|
||||||
op.active = active;
|
|
||||||
op.owner = owner;
|
|
||||||
tx.operations.push_back( op );
|
|
||||||
set_expiration( db, tx );
|
|
||||||
PUSH_TX( db, tx, database::skip_transaction_signatures );
|
|
||||||
} ;
|
|
||||||
|
|
||||||
auto get_active = [&](
|
|
||||||
account_id_type aid
|
|
||||||
) -> const authority*
|
|
||||||
{
|
|
||||||
return &(aid(db).active);
|
|
||||||
} ;
|
|
||||||
|
|
||||||
auto get_owner = [&](
|
|
||||||
account_id_type aid
|
|
||||||
) -> const authority*
|
|
||||||
{
|
|
||||||
return &(aid(db).owner);
|
|
||||||
} ;
|
|
||||||
|
|
||||||
fc::ecc::private_key alice_active_key = fc::ecc::private_key::regenerate(fc::digest("alice_active"));
|
|
||||||
fc::ecc::private_key alice_owner_key = fc::ecc::private_key::regenerate(fc::digest("alice_owner"));
|
|
||||||
public_key_type alice_active_pub( alice_active_key.get_public_key() );
|
|
||||||
public_key_type alice_owner_pub( alice_owner_key.get_public_key() );
|
|
||||||
set_auth( alice_id, authority( 1, alice_active_pub, 1 ), authority( 1, alice_owner_pub, 1 ) );
|
|
||||||
|
|
||||||
// creating a transaction that needs owner permission
|
|
||||||
signed_transaction tx;
|
|
||||||
account_update_operation op;
|
|
||||||
op.account = alice_id;
|
|
||||||
op.owner = authority( 1, alice_active_pub, 1 );
|
|
||||||
tx.operations.push_back( op );
|
|
||||||
|
|
||||||
// not signed, should throw tx_missing_owner_auth
|
|
||||||
GRAPHENE_REQUIRE_THROW( tx.verify_authority( db.get_chain_id(), get_active, get_owner, false, false ),
|
|
||||||
graphene::chain::tx_missing_owner_auth );
|
|
||||||
GRAPHENE_REQUIRE_THROW( tx.verify_authority( db.get_chain_id(), get_active, get_owner, true, false ),
|
|
||||||
graphene::chain::tx_missing_owner_auth );
|
|
||||||
|
|
||||||
// signed with alice's active key, should throw tx_missing_owner_auth
|
|
||||||
sign( tx, alice_active_key );
|
|
||||||
GRAPHENE_REQUIRE_THROW( tx.verify_authority( db.get_chain_id(), get_active, get_owner, false, false ),
|
|
||||||
graphene::chain::tx_missing_owner_auth );
|
|
||||||
GRAPHENE_REQUIRE_THROW( tx.verify_authority( db.get_chain_id(), get_active, get_owner, true, false ),
|
|
||||||
graphene::chain::tx_missing_owner_auth );
|
|
||||||
|
|
||||||
// signed with alice's owner key, should not throw
|
|
||||||
tx.clear_signatures();
|
|
||||||
sign( tx, alice_owner_key );
|
|
||||||
tx.verify_authority( db.get_chain_id(), get_active, get_owner, false, false );
|
|
||||||
tx.verify_authority( db.get_chain_id(), get_active, get_owner, true, false );
|
|
||||||
|
|
||||||
// signed with both alice's owner key and active key,
|
|
||||||
// it does not throw due to https://github.com/bitshares/bitshares-core/issues/580
|
|
||||||
sign( tx, alice_active_key );
|
|
||||||
tx.verify_authority( db.get_chain_id(), get_active, get_owner, false, false );
|
|
||||||
tx.verify_authority( db.get_chain_id(), get_active, get_owner, true, false );
|
|
||||||
|
|
||||||
// creating a transaction that needs active permission
|
|
||||||
tx.clear();
|
|
||||||
op.owner.reset();
|
|
||||||
op.active = authority( 1, alice_owner_pub, 1 );
|
|
||||||
tx.operations.push_back( op );
|
|
||||||
|
|
||||||
// not signed, should throw tx_missing_active_auth
|
|
||||||
GRAPHENE_REQUIRE_THROW( tx.verify_authority( db.get_chain_id(), get_active, get_owner, false, false ),
|
|
||||||
graphene::chain::tx_missing_active_auth );
|
|
||||||
GRAPHENE_REQUIRE_THROW( tx.verify_authority( db.get_chain_id(), get_active, get_owner, true, false ),
|
|
||||||
graphene::chain::tx_missing_active_auth );
|
|
||||||
|
|
||||||
// signed with alice's active key, should not throw
|
|
||||||
sign( tx, alice_active_key );
|
|
||||||
tx.verify_authority( db.get_chain_id(), get_active, get_owner, false, false );
|
|
||||||
tx.verify_authority( db.get_chain_id(), get_active, get_owner, true, false );
|
|
||||||
|
|
||||||
// signed with alice's owner key, should not throw
|
|
||||||
tx.clear_signatures();
|
|
||||||
sign( tx, alice_owner_key );
|
|
||||||
tx.verify_authority( db.get_chain_id(), get_active, get_owner, false, false );
|
|
||||||
tx.verify_authority( db.get_chain_id(), get_active, get_owner, true, false );
|
|
||||||
|
|
||||||
// signed with both alice's owner key and active key, should throw tx_irrelevant_sig
|
|
||||||
sign( tx, alice_active_key );
|
|
||||||
GRAPHENE_REQUIRE_THROW( tx.verify_authority( db.get_chain_id(), get_active, get_owner, false, false ),
|
|
||||||
graphene::chain::tx_irrelevant_sig );
|
|
||||||
GRAPHENE_REQUIRE_THROW( tx.verify_authority( db.get_chain_id(), get_active, get_owner, true, false ),
|
|
||||||
graphene::chain::tx_irrelevant_sig );
|
|
||||||
|
|
||||||
}
|
|
||||||
catch(fc::exception& e)
|
|
||||||
{
|
|
||||||
edump((e.to_detail_string()));
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
BOOST_AUTO_TEST_SUITE_END()
|
BOOST_AUTO_TEST_SUITE_END()
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,8 @@
|
||||||
#include <graphene/app/database_api.hpp>
|
#include <graphene/app/database_api.hpp>
|
||||||
#include <graphene/chain/hardfork.hpp>
|
#include <graphene/chain/hardfork.hpp>
|
||||||
|
|
||||||
|
#include <fc/crypto/digest.hpp>
|
||||||
|
|
||||||
#include "../common/database_fixture.hpp"
|
#include "../common/database_fixture.hpp"
|
||||||
|
|
||||||
using namespace graphene::chain;
|
using namespace graphene::chain;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue