Merge branch 'confidential'
This commit is contained in:
commit
6f19268ffd
15 changed files with 807 additions and 8 deletions
|
|
@ -24,6 +24,7 @@
|
|||
#include <graphene/chain/withdraw_permission_object.hpp>
|
||||
#include <graphene/chain/worker_evaluator.hpp>
|
||||
#include <graphene/chain/transaction_object.hpp>
|
||||
#include <graphene/chain/confidential_evaluator.hpp>
|
||||
|
||||
#include <fc/crypto/hex.hpp>
|
||||
#include <fc/smart_ref_impl.hpp>
|
||||
|
|
@ -684,6 +685,13 @@ namespace graphene { namespace app {
|
|||
result.reserve( impacted.size() );
|
||||
for( auto& item : impacted ) result.emplace_back(item);
|
||||
break;
|
||||
} case impl_blinded_balance_object_type:{
|
||||
const auto& aobj = dynamic_cast<const blinded_balance_object*>(obj);
|
||||
assert( aobj != nullptr );
|
||||
result.reserve( aobj->owner.account_auths.size() );
|
||||
for( const auto& a : aobj->owner.account_auths )
|
||||
result.push_back( a.first );
|
||||
break;
|
||||
} case impl_block_summary_object_type:{
|
||||
} case impl_account_transaction_history_object_type:{
|
||||
} case impl_witness_schedule_object_type: {
|
||||
|
|
@ -1088,5 +1096,18 @@ namespace graphene { namespace app {
|
|||
_db.get_global_properties().parameters.max_authority_depth );
|
||||
return true;
|
||||
}
|
||||
vector<blinded_balance_object> database_api::get_blinded_balances( const flat_set<commitment_type>& commitments )const
|
||||
{
|
||||
vector<blinded_balance_object> result; result.reserve(commitments.size());
|
||||
const auto& bal_idx = _db.get_index_type<blinded_balance_index>();
|
||||
const auto& by_commitment_idx = bal_idx.indices().get<by_commitment>();
|
||||
for( const auto& c : commitments )
|
||||
{
|
||||
auto itr = by_commitment_idx.find( c );
|
||||
if( itr != by_commitment_idx.end() )
|
||||
result.push_back( *itr );
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} } // graphene::app
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@
|
|||
#include <graphene/chain/witness_object.hpp>
|
||||
#include <graphene/chain/proposal_object.hpp>
|
||||
#include <graphene/chain/balance_object.hpp>
|
||||
#include <graphene/chain/confidential_evaluator.hpp>
|
||||
#include <graphene/net/node.hpp>
|
||||
|
||||
#include <graphene/market_history/market_history_plugin.hpp>
|
||||
|
|
@ -319,6 +320,12 @@ namespace graphene { namespace app {
|
|||
bool verify_authority( const signed_transaction& trx )const;
|
||||
|
||||
|
||||
/**
|
||||
* @return the set of blinded balance objects by commitment ID
|
||||
*/
|
||||
vector<blinded_balance_object> get_blinded_balances( const flat_set<commitment_type>& commitments )const;
|
||||
|
||||
|
||||
private:
|
||||
/** called every time a block is applied to report the objects that were changed */
|
||||
void on_objects_changed(const vector<object_id_type>& ids);
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ add_library( graphene_chain
|
|||
protocol/transaction.cpp
|
||||
protocol/block.cpp
|
||||
protocol/fee_schedule.cpp
|
||||
protocol/confidential.cpp
|
||||
|
||||
pts_address.cpp
|
||||
|
||||
|
|
@ -37,6 +38,7 @@ add_library( graphene_chain
|
|||
vesting_balance_evaluator.cpp
|
||||
withdraw_permission_evaluator.cpp
|
||||
worker_evaluator.cpp
|
||||
confidential_evaluator.cpp
|
||||
|
||||
account_object.cpp
|
||||
asset_object.cpp
|
||||
|
|
|
|||
137
libraries/chain/confidential_evaluator.cpp
Normal file
137
libraries/chain/confidential_evaluator.cpp
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
#include <graphene/chain/exceptions.hpp>
|
||||
#include <graphene/chain/protocol/confidential.hpp>
|
||||
#include <graphene/chain/confidential_evaluator.hpp>
|
||||
#include <graphene/chain/database.hpp>
|
||||
|
||||
namespace graphene { namespace chain {
|
||||
|
||||
void_result transfer_to_blind_evaluator::do_evaluate( const transfer_to_blind_operation& o )
|
||||
{ try {
|
||||
const auto& d = db();
|
||||
|
||||
const auto& atype = o.amount.asset_id(db());
|
||||
FC_ASSERT( atype.allow_confidential() );
|
||||
FC_ASSERT( !atype.is_transfer_restricted() );
|
||||
FC_ASSERT( !atype.enforce_white_list() );
|
||||
|
||||
for( const auto& out : o.outputs )
|
||||
{
|
||||
for( const auto& a : out.owner.account_auths )
|
||||
a.first(d); // verify all accounts exist and are valid
|
||||
}
|
||||
return void_result();
|
||||
} FC_CAPTURE_AND_RETHROW( (o) ) }
|
||||
|
||||
|
||||
void_result transfer_to_blind_evaluator::do_apply( const transfer_to_blind_operation& o )
|
||||
{ try {
|
||||
db().adjust_balance( o.from, -o.amount );
|
||||
|
||||
const auto& add = o.amount.asset_id(db()).dynamic_asset_data_id(db()); // verify fee is a legit asset
|
||||
db().modify( add, [&]( asset_dynamic_data_object& obj ){
|
||||
obj.confidential_supply += o.amount.amount;
|
||||
FC_ASSERT( obj.confidential_supply >= 0 );
|
||||
});
|
||||
for( const auto& out : o.outputs )
|
||||
{
|
||||
db().create<blinded_balance_object>( [&]( blinded_balance_object& obj ){
|
||||
obj.asset_id = o.amount.asset_id;
|
||||
obj.owner = out.owner;
|
||||
obj.commitment = out.commitment;
|
||||
});
|
||||
}
|
||||
return void_result();
|
||||
} FC_CAPTURE_AND_RETHROW( (o) ) }
|
||||
|
||||
|
||||
void_result transfer_from_blind_evaluator::do_evaluate( const transfer_from_blind_operation& o )
|
||||
{ try {
|
||||
const auto& d = db();
|
||||
o.fee.asset_id(d); // verify fee is a legit asset
|
||||
const auto& bbi = d.get_index_type<blinded_balance_index>();
|
||||
const auto& cidx = bbi.indices().get<by_commitment>();
|
||||
for( const auto& in : o.inputs )
|
||||
{
|
||||
auto itr = cidx.find( in.commitment );
|
||||
FC_ASSERT( itr != cidx.end() );
|
||||
FC_ASSERT( itr->asset_id == o.fee.asset_id );
|
||||
FC_ASSERT( itr->owner == in.owner );
|
||||
}
|
||||
return void_result();
|
||||
} FC_CAPTURE_AND_RETHROW( (o) ) }
|
||||
|
||||
void_result transfer_from_blind_evaluator::do_apply( const transfer_from_blind_operation& o )
|
||||
{ try {
|
||||
db().adjust_balance( o.fee_payer(), o.fee );
|
||||
db().adjust_balance( o.to, o.amount );
|
||||
const auto& bbi = db().get_index_type<blinded_balance_index>();
|
||||
const auto& cidx = bbi.indices().get<by_commitment>();
|
||||
for( const auto& in : o.inputs )
|
||||
{
|
||||
auto itr = cidx.find( in.commitment );
|
||||
FC_ASSERT( itr != cidx.end() );
|
||||
db().remove( *itr );
|
||||
}
|
||||
const auto& add = o.amount.asset_id(db()).dynamic_asset_data_id(db()); // verify fee is a legit asset
|
||||
db().modify( add, [&]( asset_dynamic_data_object& obj ){
|
||||
obj.confidential_supply -= o.amount.amount + o.fee.amount;
|
||||
FC_ASSERT( obj.confidential_supply >= 0 );
|
||||
});
|
||||
return void_result();
|
||||
} FC_CAPTURE_AND_RETHROW( (o) ) }
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
void_result blind_transfer_evaluator::do_evaluate( const blind_transfer_operation& o )
|
||||
{ try {
|
||||
const auto& d = db();
|
||||
o.fee.asset_id(db()); // verify fee is a legit asset
|
||||
const auto& bbi = db().get_index_type<blinded_balance_index>();
|
||||
const auto& cidx = bbi.indices().get<by_commitment>();
|
||||
for( const auto& out : o.outputs )
|
||||
{
|
||||
for( const auto& a : out.owner.account_auths )
|
||||
a.first(d); // verify all accounts exist and are valid
|
||||
}
|
||||
for( const auto& in : o.inputs )
|
||||
{
|
||||
auto itr = cidx.find( in.commitment );
|
||||
GRAPHENE_ASSERT( itr != cidx.end(), blind_transfer_unknown_commitment, "", ("commitment",in.commitment) );
|
||||
FC_ASSERT( itr->asset_id == o.fee.asset_id );
|
||||
FC_ASSERT( itr->owner == in.owner );
|
||||
}
|
||||
return void_result();
|
||||
} FC_CAPTURE_AND_RETHROW( (o) ) }
|
||||
|
||||
void_result blind_transfer_evaluator::do_apply( const blind_transfer_operation& o )
|
||||
{ try {
|
||||
db().adjust_balance( o.fee_payer(), o.fee ); // deposit the fee to the temp account
|
||||
const auto& bbi = db().get_index_type<blinded_balance_index>();
|
||||
const auto& cidx = bbi.indices().get<by_commitment>();
|
||||
for( const auto& in : o.inputs )
|
||||
{
|
||||
auto itr = cidx.find( in.commitment );
|
||||
GRAPHENE_ASSERT( itr != cidx.end(), blind_transfer_unknown_commitment, "", ("commitment",in.commitment) );
|
||||
db().remove( *itr );
|
||||
}
|
||||
for( const auto& out : o.outputs )
|
||||
{
|
||||
db().create<blinded_balance_object>( [&]( blinded_balance_object& obj ){
|
||||
obj.asset_id = o.fee.asset_id;
|
||||
obj.owner = out.owner;
|
||||
obj.commitment = out.commitment;
|
||||
});
|
||||
}
|
||||
const auto& add = o.fee.asset_id(db()).dynamic_asset_data_id(db());
|
||||
db().modify( add, [&]( asset_dynamic_data_object& obj ){
|
||||
obj.confidential_supply -= o.fee.amount;
|
||||
FC_ASSERT( obj.confidential_supply >= 0 );
|
||||
});
|
||||
|
||||
return void_result();
|
||||
} FC_CAPTURE_AND_RETHROW( (o) ) }
|
||||
|
||||
|
||||
} } // graphene::chain
|
||||
|
|
@ -44,6 +44,7 @@
|
|||
#include <graphene/chain/witness_evaluator.hpp>
|
||||
#include <graphene/chain/worker_evaluator.hpp>
|
||||
#include <graphene/chain/balance_evaluator.hpp>
|
||||
#include <graphene/chain/confidential_evaluator.hpp>
|
||||
|
||||
#include <graphene/chain/protocol/fee_schedule.hpp>
|
||||
|
||||
|
|
@ -150,6 +151,9 @@ void database::initialize_evaluators()
|
|||
register_evaluator<withdraw_permission_delete_evaluator>();
|
||||
register_evaluator<worker_create_evaluator>();
|
||||
register_evaluator<balance_claim_evaluator>();
|
||||
register_evaluator<transfer_to_blind_evaluator>();
|
||||
register_evaluator<transfer_from_blind_evaluator>();
|
||||
register_evaluator<blind_transfer_evaluator>();
|
||||
}
|
||||
|
||||
void database::initialize_indexes()
|
||||
|
|
@ -177,6 +181,7 @@ void database::initialize_indexes()
|
|||
add_index< primary_index<vesting_balance_index> >();
|
||||
add_index< primary_index<worker_index> >();
|
||||
add_index< primary_index<balance_index> >();
|
||||
add_index< primary_index<blinded_balance_index> >();
|
||||
|
||||
//Implementation object indexes
|
||||
add_index< primary_index<transaction_index > >();
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ namespace graphene { namespace chain {
|
|||
|
||||
/// The number of shares currently in existence
|
||||
share_type current_supply;
|
||||
share_type confidential_supply; ///< total asset held in confidential balances
|
||||
share_type accumulated_fees; ///< fees accumulate to be paid out over time
|
||||
share_type fee_pool; ///< in core asset
|
||||
};
|
||||
|
|
@ -91,6 +92,7 @@ namespace graphene { namespace chain {
|
|||
/// @return true if this asset may only be transferred to/from the issuer or market orders
|
||||
bool is_transfer_restricted()const { return options.flags & transfer_restricted; }
|
||||
bool can_override()const { return options.flags & override_authority; }
|
||||
bool allow_confidential()const { return !(options.flags & asset_issuer_permission_flags::disable_confidential); }
|
||||
|
||||
/// Helper function to get an asset object with the given amount in this asset's type
|
||||
asset amount(share_type a)const { return asset(a, id); }
|
||||
|
|
@ -234,7 +236,7 @@ namespace graphene { namespace chain {
|
|||
|
||||
} } // graphene::chain
|
||||
FC_REFLECT_DERIVED( graphene::chain::asset_dynamic_data_object, (graphene::db::object),
|
||||
(current_supply)(accumulated_fees)(fee_pool) )
|
||||
(current_supply)(confidential_supply)(accumulated_fees)(fee_pool) )
|
||||
|
||||
FC_REFLECT_DERIVED( graphene::chain::asset_bitasset_data_object, (graphene::db::object),
|
||||
(feeds)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,71 @@
|
|||
#pragma once
|
||||
#include <graphene/chain/evaluator.hpp>
|
||||
#include <graphene/db/object.hpp>
|
||||
#include <graphene/db/generic_index.hpp>
|
||||
|
||||
namespace graphene { namespace chain {
|
||||
|
||||
/**
|
||||
* @class blinded_balance_object
|
||||
* @brief tracks a blinded balance commitment
|
||||
* @ingroup object
|
||||
* @ingroup protocol
|
||||
*/
|
||||
class blinded_balance_object : public graphene::db::abstract_object<blinded_balance_object>
|
||||
{
|
||||
public:
|
||||
static const uint8_t space_id = implementation_ids;
|
||||
static const uint8_t type_id = impl_blinded_balance_object_type;
|
||||
|
||||
fc::ecc::commitment_type commitment;
|
||||
asset_id_type asset_id;
|
||||
authority owner;
|
||||
};
|
||||
|
||||
struct by_asset;
|
||||
struct by_owner;
|
||||
struct by_commitment;
|
||||
|
||||
/**
|
||||
* @ingroup object_index
|
||||
*/
|
||||
typedef multi_index_container<
|
||||
blinded_balance_object,
|
||||
indexed_by<
|
||||
ordered_unique< tag<by_id>, member< object, object_id_type, &object::id > >,
|
||||
ordered_unique< tag<by_commitment>, member<blinded_balance_object, commitment_type, &blinded_balance_object::commitment> >
|
||||
>
|
||||
> blinded_balance_object_multi_index_type;
|
||||
typedef generic_index<blinded_balance_object, blinded_balance_object_multi_index_type> blinded_balance_index;
|
||||
|
||||
|
||||
class transfer_to_blind_evaluator : public evaluator<transfer_to_blind_evaluator>
|
||||
{
|
||||
public:
|
||||
typedef transfer_to_blind_operation operation_type;
|
||||
|
||||
void_result do_evaluate( const transfer_to_blind_operation& o );
|
||||
void_result do_apply( const transfer_to_blind_operation& o ) ;
|
||||
};
|
||||
|
||||
class transfer_from_blind_evaluator : public evaluator<transfer_from_blind_evaluator>
|
||||
{
|
||||
public:
|
||||
typedef transfer_from_blind_operation operation_type;
|
||||
|
||||
void_result do_evaluate( const transfer_from_blind_operation& o );
|
||||
void_result do_apply( const transfer_from_blind_operation& o ) ;
|
||||
};
|
||||
|
||||
class blind_transfer_evaluator : public evaluator<blind_transfer_evaluator>
|
||||
{
|
||||
public:
|
||||
typedef blind_transfer_operation operation_type;
|
||||
|
||||
void_result do_evaluate( const blind_transfer_operation& o );
|
||||
void_result do_apply( const blind_transfer_operation& o ) ;
|
||||
};
|
||||
|
||||
} } // namespace graphene::chain
|
||||
|
||||
FC_REFLECT( graphene::chain::blinded_balance_object, (commitment)(asset_id)(owner) )
|
||||
|
|
@ -143,6 +143,10 @@ namespace graphene { namespace chain {
|
|||
|
||||
GRAPHENE_DECLARE_OP_EVALUATE_EXCEPTION( not_permitted, override_transfer, 1, "not permitted" )
|
||||
|
||||
|
||||
GRAPHENE_DECLARE_OP_BASE_EXCEPTIONS( blind_transfer );
|
||||
GRAPHENE_DECLARE_OP_EVALUATE_EXCEPTION( unknown_commitment, blind_transfer, 1, "Attempting to claim an unknown prior commitment" );
|
||||
|
||||
/*
|
||||
FC_DECLARE_DERIVED_EXCEPTION( addition_overflow, graphene::chain::chain_exception, 30002, "addition overflow" )
|
||||
FC_DECLARE_DERIVED_EXCEPTION( subtraction_overflow, graphene::chain::chain_exception, 30003, "subtraction overflow" )
|
||||
|
|
|
|||
|
|
@ -83,6 +83,14 @@ namespace graphene { namespace chain {
|
|||
result.push_back(k.first);
|
||||
return result;
|
||||
}
|
||||
|
||||
friend bool operator == ( const authority& a, const authority& b )
|
||||
{
|
||||
return (a.weight_threshold == b.weight_threshold) &&
|
||||
(a.account_auths == b.account_auths) &&
|
||||
(a.key_auths == b.key_auths) &&
|
||||
(a.address_auths == b.address_auths);
|
||||
}
|
||||
uint32_t num_auths()const { return account_auths.size() + key_auths.size() + address_auths.size(); }
|
||||
void clear() { account_auths.clear(); key_auths.clear(); }
|
||||
|
||||
|
|
|
|||
284
libraries/chain/include/graphene/chain/protocol/confidential.hpp
Normal file
284
libraries/chain/include/graphene/chain/protocol/confidential.hpp
Normal file
|
|
@ -0,0 +1,284 @@
|
|||
/*
|
||||
* Copyright (c) 2015, Cryptonomex, Inc.
|
||||
* All rights reserved.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <graphene/chain/protocol/base.hpp>
|
||||
|
||||
namespace graphene { namespace chain {
|
||||
|
||||
using fc::ecc::blind_factor_type;
|
||||
|
||||
/**
|
||||
* @defgroup stealth Stealth Transfer
|
||||
* @brief Operations related to stealth transfer of value
|
||||
*
|
||||
* Stealth Transfers enable users to maintain their finanical privacy against even
|
||||
* though all transactions are public. Every account has three balances:
|
||||
*
|
||||
* 1. Public Balance - everyone can see the balance changes and the parties involved
|
||||
* 2. Blinded Balance - everyone can see who is transacting but not the amounts involved
|
||||
* 3. Stealth Balance - both the amounts and parties involved are obscured
|
||||
*
|
||||
* Account owners may set a flag that allows their account to receive(or not) transfers of these kinds
|
||||
* Asset issuers can enable or disable the use of each of these types of accounts.
|
||||
*
|
||||
* Using the "temp account" which has no permissions required, users can transfer a
|
||||
* stealth balance to the temp account and then use the temp account to register a new
|
||||
* account. In this way users can use stealth funds to create anonymous accounts with which
|
||||
* they can perform other actions that are not compatible with blinded balances (such as market orders)
|
||||
*
|
||||
* @section referral_program Referral Progam
|
||||
*
|
||||
* Stealth transfers that do not specify any account id cannot pay referral fees so 100% of the
|
||||
* transaction fee is paid to the network.
|
||||
*
|
||||
* @section transaction_fees Fees
|
||||
*
|
||||
* Stealth transfers can have an arbitrarylly large size and therefore the transaction fee for
|
||||
* stealth transfers is based purley on the data size of the transaction.
|
||||
*/
|
||||
///@{
|
||||
|
||||
/**
|
||||
* @ingroup stealth
|
||||
* This data is encrypted and stored in the
|
||||
* encrypted memo portion of the blind output.
|
||||
*/
|
||||
struct blind_memo
|
||||
{
|
||||
account_id_type from;
|
||||
share_type amount;
|
||||
string message;
|
||||
/** set to the first 4 bytes of the shared secret
|
||||
* used to encrypt the memo. Used to verify that
|
||||
* decryption was successful.
|
||||
*/
|
||||
uint32_t check= 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* @ingroup stealth
|
||||
*/
|
||||
struct blind_input
|
||||
{
|
||||
fc::ecc::commitment_type commitment;
|
||||
/** provided to maintain the invariant that all authority
|
||||
* required by an operation is explicit in the operation. Must
|
||||
* match blinded_balance_id->owner
|
||||
*/
|
||||
authority owner;
|
||||
};
|
||||
|
||||
/**
|
||||
* When sending a stealth tranfer we assume users are unable to scan
|
||||
* the full blockchain; therefore, payments require confirmation data
|
||||
* to be passed out of band. We assume this out-of-band channel is
|
||||
* not secure and therefore the contents of the confirmation must be
|
||||
* encrypted.
|
||||
*/
|
||||
struct stealth_confirmation
|
||||
{
|
||||
struct memo_data
|
||||
{
|
||||
public_key_type from;
|
||||
asset amount;
|
||||
fc::ecc::commitment_type commitment;
|
||||
uint32_t check = 0;
|
||||
};
|
||||
|
||||
/**
|
||||
* Packs *this then encodes as base58 encoded string.
|
||||
*/
|
||||
operator string()const;
|
||||
/**
|
||||
* Unpacks from a base58 string
|
||||
*/
|
||||
stealth_confirmation( const std::string& base58 );
|
||||
stealth_confirmation(){}
|
||||
|
||||
public_key_type one_time_key;
|
||||
vector<char> encrypted_memo;
|
||||
};
|
||||
|
||||
/**
|
||||
* @class blind_output
|
||||
* @brief Defines data required to create a new blind commitment
|
||||
* @ingroup stealth
|
||||
*
|
||||
* The blinded output that must be proven to be greater than 0
|
||||
*/
|
||||
struct blind_output
|
||||
{
|
||||
fc::ecc::commitment_type commitment;
|
||||
/** only required if there is more than one blind output */
|
||||
range_proof_type range_proof;
|
||||
authority owner;
|
||||
optional<stealth_confirmation> stealth_memo;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @class transfer_to_blind_operation
|
||||
* @ingroup stealth
|
||||
* @brief Converts public account balance to a blinded or stealth balance
|
||||
*/
|
||||
struct transfer_to_blind_operation : public base_operation
|
||||
{
|
||||
struct fee_parameters_type {
|
||||
uint64_t fee = 5*GRAPHENE_BLOCKCHAIN_PRECISION; ///< the cost to register the cheapest non-free account
|
||||
uint32_t price_per_output = 5*GRAPHENE_BLOCKCHAIN_PRECISION;
|
||||
uint32_t price_per_kb = 5*GRAPHENE_BLOCKCHAIN_PRECISION;
|
||||
};
|
||||
|
||||
|
||||
asset fee;
|
||||
asset amount;
|
||||
account_id_type from;
|
||||
blind_factor_type blinding_factor;
|
||||
vector<blind_output> outputs;
|
||||
|
||||
account_id_type fee_payer()const { return from; }
|
||||
void validate()const;
|
||||
share_type calculate_fee(const fee_parameters_type& )const;
|
||||
|
||||
void get_impacted_accounts( flat_set<account_id_type>& i )const
|
||||
{
|
||||
i.insert(from);
|
||||
for( const auto& out : outputs )
|
||||
add_authority_accounts( i, out.owner );
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @ingroup stealth
|
||||
* @brief Converts blinded/stealth balance to a public account balance
|
||||
*/
|
||||
struct transfer_from_blind_operation : public base_operation
|
||||
{
|
||||
struct fee_parameters_type {
|
||||
uint64_t fee = 5*GRAPHENE_BLOCKCHAIN_PRECISION; ///< the cost to register the cheapest non-free account
|
||||
};
|
||||
|
||||
asset fee;
|
||||
asset amount;
|
||||
account_id_type to;
|
||||
blind_factor_type blinding_factor;
|
||||
vector<blind_input> inputs;
|
||||
|
||||
account_id_type fee_payer()const { return GRAPHENE_TEMP_ACCOUNT; }
|
||||
void validate()const;
|
||||
|
||||
void get_impacted_accounts( flat_set<account_id_type>& i )const
|
||||
{
|
||||
i.insert(to);
|
||||
for( const auto& in : inputs )
|
||||
add_authority_accounts( i, in.owner );
|
||||
}
|
||||
void get_required_authorities( vector<authority>& a )const
|
||||
{
|
||||
for( const auto& in : inputs )
|
||||
a.push_back( in.owner );
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @ingroup stealth
|
||||
* @brief Transfers from blind to blind
|
||||
*
|
||||
* There are two ways to transfer value while maintaining privacy:
|
||||
* 1. account to account with amount kept secret
|
||||
* 2. stealth transfers with amount sender/receiver kept secret
|
||||
*
|
||||
* When doing account to account transfers, everyone with access to the
|
||||
* memo key can see the amounts, but they will not have access to the funds.
|
||||
*
|
||||
* When using stealth transfers the same key is used for control and reading
|
||||
* the memo.
|
||||
*
|
||||
* This operation is more expensive than a normal transfer and has
|
||||
* a fee proportional to the size of the operation.
|
||||
*
|
||||
* All assets in a blind transfer must be of the same type: fee.asset_id
|
||||
* The fee_payer is the temp account and can be funded from the blinded values.
|
||||
*
|
||||
* Using this operation you can transfer from an account and/or blinded balances
|
||||
* to an account and/or blinded balances.
|
||||
*
|
||||
* Stealth Transfers:
|
||||
*
|
||||
* Assuming Receiver has key pair R,r and has shared public key R with Sender
|
||||
* Assuming Sender has key pair S,s
|
||||
* Generate one time key pair O,o as s.child(nonce) where nonce can be inferred from transaction
|
||||
* Calculate secret V = o*R
|
||||
* blinding_factor = sha256(V)
|
||||
* memo is encrypted via aes of V
|
||||
* owner = R.child(sha256(blinding_factor))
|
||||
*
|
||||
* Sender gives Receiver output ID to complete the payment.
|
||||
*
|
||||
* This process can also be used to send money to a cold wallet without having to
|
||||
* pre-register any accounts.
|
||||
*
|
||||
* Outputs are assigned the same IDs as the inputs until no more input IDs are available,
|
||||
* in which case a the return value will be the *first* ID allocated for an output. Additional
|
||||
* output IDs are allocated sequentially thereafter. If there are fewer outputs than inputs
|
||||
* then the input IDs are freed and never used again.
|
||||
*/
|
||||
struct blind_transfer_operation : public base_operation
|
||||
{
|
||||
struct fee_parameters_type {
|
||||
uint64_t fee = 5*GRAPHENE_BLOCKCHAIN_PRECISION; ///< the cost to register the cheapest non-free account
|
||||
uint32_t price_per_output = 5*GRAPHENE_BLOCKCHAIN_PRECISION;
|
||||
uint32_t price_per_kb = 5*GRAPHENE_BLOCKCHAIN_PRECISION;
|
||||
};
|
||||
|
||||
asset fee;
|
||||
vector<blind_input> inputs;
|
||||
vector<blind_output> outputs;
|
||||
|
||||
/** graphene TEMP account */
|
||||
account_id_type fee_payer()const;
|
||||
void validate()const;
|
||||
share_type calculate_fee( const fee_parameters_type& k )const;
|
||||
|
||||
void get_impacted_accounts( flat_set<account_id_type>& i )const
|
||||
{
|
||||
for( const auto& in : inputs )
|
||||
add_authority_accounts( i, in.owner );
|
||||
for( const auto& out : outputs )
|
||||
add_authority_accounts( i, out.owner );
|
||||
}
|
||||
void get_required_authorities( vector<authority>& a )const
|
||||
{
|
||||
for( const auto& in : inputs )
|
||||
a.push_back( in.owner );
|
||||
}
|
||||
};
|
||||
|
||||
///@} endgroup stealth
|
||||
} } // graphene::chain
|
||||
|
||||
FC_REFLECT( graphene::chain::stealth_confirmation,
|
||||
(one_time_key)(encrypted_memo) )
|
||||
|
||||
FC_REFLECT( graphene::chain::stealth_confirmation::memo_data,
|
||||
(from)(amount)(commitment)(check) );
|
||||
|
||||
FC_REFLECT( graphene::chain::blind_memo,
|
||||
(from)(amount)(message)(check) )
|
||||
FC_REFLECT( graphene::chain::blind_input,
|
||||
(commitment)(owner) )
|
||||
FC_REFLECT( graphene::chain::blind_output,
|
||||
(commitment)(range_proof)(owner)(stealth_memo) )
|
||||
FC_REFLECT( graphene::chain::transfer_to_blind_operation,
|
||||
(fee)(amount)(from)(blinding_factor)(outputs) )
|
||||
FC_REFLECT( graphene::chain::transfer_from_blind_operation,
|
||||
(fee)(amount)(to)(blinding_factor)(inputs) )
|
||||
FC_REFLECT( graphene::chain::blind_transfer_operation,
|
||||
(fee)(inputs)(outputs) )
|
||||
FC_REFLECT( graphene::chain::transfer_to_blind_operation::fee_parameters_type, (fee)(price_per_output)(price_per_kb) )
|
||||
FC_REFLECT( graphene::chain::transfer_from_blind_operation::fee_parameters_type, (fee) )
|
||||
FC_REFLECT( graphene::chain::blind_transfer_operation::fee_parameters_type, (fee)(price_per_output)(price_per_kb) )
|
||||
|
||||
|
|
@ -14,6 +14,7 @@
|
|||
#include <graphene/chain/protocol/withdraw_permission.hpp>
|
||||
#include <graphene/chain/protocol/witness.hpp>
|
||||
#include <graphene/chain/protocol/worker.hpp>
|
||||
#include <graphene/chain/protocol/confidential.hpp>
|
||||
|
||||
namespace graphene { namespace chain {
|
||||
|
||||
|
|
@ -59,7 +60,10 @@ namespace graphene { namespace chain {
|
|||
custom_operation,
|
||||
assert_operation,
|
||||
balance_claim_operation,
|
||||
override_transfer_operation
|
||||
override_transfer_operation,
|
||||
transfer_to_blind_operation,
|
||||
blind_transfer_operation,
|
||||
transfer_from_blind_operation
|
||||
> operation;
|
||||
|
||||
/// @} // operations group
|
||||
|
|
|
|||
|
|
@ -84,10 +84,11 @@ namespace graphene { namespace chain {
|
|||
override_authority = 0x04, /**< issuer may transfer asset back to himself */
|
||||
transfer_restricted = 0x08, /**< require the issuer to be one party to every transfer */
|
||||
disable_force_settle = 0x10, /**< disable force settling */
|
||||
global_settle = 0x20 /**< allow the bitasset issuer to force a global settling -- this may be set in permissions, but not flags */
|
||||
global_settle = 0x20, /**< allow the bitasset issuer to force a global settling -- this may be set in permissions, but not flags */
|
||||
disable_confidential = 0x40 /**< allow the asset to be used with confidential transactions */
|
||||
};
|
||||
const static uint32_t ASSET_ISSUER_PERMISSION_MASK = charge_market_fee|white_list|override_authority|transfer_restricted|disable_force_settle|global_settle;
|
||||
const static uint32_t UIA_ASSET_ISSUER_PERMISSION_MASK = charge_market_fee|white_list|override_authority|transfer_restricted;
|
||||
const static uint32_t ASSET_ISSUER_PERMISSION_MASK = charge_market_fee|white_list|override_authority|transfer_restricted|disable_force_settle|global_settle|disable_confidential;
|
||||
const static uint32_t UIA_ASSET_ISSUER_PERMISSION_MASK = charge_market_fee|white_list|override_authority|transfer_restricted|disable_confidential;
|
||||
|
||||
enum reserved_spaces
|
||||
{
|
||||
|
|
@ -138,7 +139,8 @@ namespace graphene { namespace chain {
|
|||
impl_transaction_object_type,
|
||||
impl_block_summary_object_type,
|
||||
impl_account_transaction_history_object_type,
|
||||
impl_witness_schedule_object_type
|
||||
impl_witness_schedule_object_type,
|
||||
impl_blinded_balance_object_type
|
||||
};
|
||||
|
||||
enum meta_info_object_type
|
||||
|
|
@ -386,6 +388,7 @@ FC_REFLECT_ENUM( graphene::chain::impl_object_type,
|
|||
(impl_block_summary_object_type)
|
||||
(impl_account_transaction_history_object_type)
|
||||
(impl_witness_schedule_object_type)
|
||||
(impl_blinded_balance_object_type)
|
||||
)
|
||||
|
||||
FC_REFLECT_ENUM( graphene::chain::meta_info_object_type, (meta_account_object_type)(meta_asset_object_type) )
|
||||
|
|
@ -417,4 +420,4 @@ FC_REFLECT_TYPENAME( graphene::chain::account_transaction_history_id_type )
|
|||
FC_REFLECT_TYPENAME( graphene::chain::witness_schedule_id_type )
|
||||
FC_REFLECT( graphene::chain::void_t, )
|
||||
|
||||
FC_REFLECT_ENUM( graphene::chain::asset_issuer_permission_flags, (charge_market_fee)(white_list)(transfer_restricted)(override_authority)(disable_force_settle)(global_settle) )
|
||||
FC_REFLECT_ENUM( graphene::chain::asset_issuer_permission_flags, (charge_market_fee)(white_list)(transfer_restricted)(override_authority)(disable_force_settle)(global_settle)(disable_confidential) )
|
||||
|
|
|
|||
122
libraries/chain/protocol/confidential.cpp
Normal file
122
libraries/chain/protocol/confidential.cpp
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
#include <graphene/chain/protocol/confidential.hpp>
|
||||
#include <graphene/chain/confidential_evaluator.hpp>
|
||||
#include <graphene/chain/database.hpp>
|
||||
|
||||
namespace graphene { namespace chain {
|
||||
|
||||
void transfer_to_blind_operation::validate()const
|
||||
{
|
||||
FC_ASSERT( fee.amount >= 0 );
|
||||
FC_ASSERT( amount.amount > 0 );
|
||||
|
||||
vector<commitment_type> in;
|
||||
vector<commitment_type> out(outputs.size());
|
||||
int64_t net_public = amount.amount.value;
|
||||
for( uint32_t i = 0; i < out.size(); ++i )
|
||||
{
|
||||
out[i] = outputs[i].commitment;
|
||||
/// require all outputs to be sorted prevents duplicates AND prevents implementations
|
||||
/// from accidentally leaking information by how they arrange commitments.
|
||||
if( i > 0 ) FC_ASSERT( out[i-1] < out[i], "all outputs must be sorted by commitment id" );
|
||||
FC_ASSERT( !outputs[i].owner.is_impossible() );
|
||||
}
|
||||
FC_ASSERT( out.size(), "there must be at least one output" );
|
||||
|
||||
auto public_c = fc::ecc::blind(blinding_factor,net_public);
|
||||
|
||||
FC_ASSERT( fc::ecc::verify_sum( {public_c}, out, 0 ), "", ("net_public",net_public) );
|
||||
|
||||
if( outputs.size() > 1 )
|
||||
{
|
||||
for( auto out : outputs )
|
||||
{
|
||||
auto info = fc::ecc::range_get_info( out.range_proof );
|
||||
FC_ASSERT( info.max_value <= GRAPHENE_MAX_SHARE_SUPPLY );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
share_type transfer_to_blind_operation::calculate_fee( const fee_parameters_type& k )const
|
||||
{
|
||||
return k.fee + outputs.size() * k.price_per_output + calculate_data_fee( fc::raw::pack_size(*this), k.price_per_kb );
|
||||
}
|
||||
|
||||
|
||||
void transfer_from_blind_operation::validate()const
|
||||
{
|
||||
FC_ASSERT( amount.amount > 0 );
|
||||
FC_ASSERT( fee.amount >= 0 );
|
||||
FC_ASSERT( inputs.size() > 0 );
|
||||
FC_ASSERT( amount.asset_id == fee.asset_id );
|
||||
|
||||
|
||||
vector<commitment_type> in(inputs.size());
|
||||
vector<commitment_type> out;
|
||||
int64_t net_public = fee.amount.value + amount.amount.value;
|
||||
out.push_back( fc::ecc::blind( blinding_factor, net_public ) );
|
||||
for( uint32_t i = 0; i < in.size(); ++i )
|
||||
{
|
||||
in[i] = inputs[i].commitment;
|
||||
/// by requiring all inputs to be sorted we also prevent duplicate commitments on the input
|
||||
if( i > 0 ) FC_ASSERT( in[i-1] < in[i], "all inputs must be sorted by commitment id" );
|
||||
}
|
||||
FC_ASSERT( in.size(), "there must be at least one input" );
|
||||
FC_ASSERT( fc::ecc::verify_sum( in, out, 0 ) );
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* If fee_payer = temp_account_id, then the fee is paid by the surplus balance of inputs-outputs and
|
||||
* 100% of the fee goes to the network.
|
||||
*/
|
||||
account_id_type blind_transfer_operation::fee_payer()const
|
||||
{
|
||||
return GRAPHENE_TEMP_ACCOUNT;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This method can be computationally intensive because it verifies that input commitments - output commitments add up to 0
|
||||
*/
|
||||
void blind_transfer_operation::validate()const
|
||||
{ try {
|
||||
vector<commitment_type> in(inputs.size());
|
||||
vector<commitment_type> out(outputs.size());
|
||||
int64_t net_public = fee.amount.value;//from_amount.value - to_amount.value;
|
||||
for( uint32_t i = 0; i < in.size(); ++i )
|
||||
{
|
||||
in[i] = inputs[i].commitment;
|
||||
/// by requiring all inputs to be sorted we also prevent duplicate commitments on the input
|
||||
if( i > 0 ) FC_ASSERT( in[i-1] < in[i] );
|
||||
}
|
||||
for( uint32_t i = 0; i < out.size(); ++i )
|
||||
{
|
||||
out[i] = outputs[i].commitment;
|
||||
if( i > 0 ) FC_ASSERT( out[i-1] < out[i] );
|
||||
FC_ASSERT( !outputs[i].owner.is_impossible() );
|
||||
}
|
||||
FC_ASSERT( in.size(), "there must be at least one input" );
|
||||
FC_ASSERT( fc::ecc::verify_sum( in, out, net_public ), "", ("net_public", net_public) );
|
||||
|
||||
if( outputs.size() > 1 )
|
||||
{
|
||||
for( auto out : outputs )
|
||||
{
|
||||
auto info = fc::ecc::range_get_info( out.range_proof );
|
||||
FC_ASSERT( info.max_value <= GRAPHENE_MAX_SHARE_SUPPLY );
|
||||
}
|
||||
}
|
||||
} FC_CAPTURE_AND_RETHROW( (*this) ) }
|
||||
|
||||
share_type blind_transfer_operation::calculate_fee( const fee_parameters_type& k )const
|
||||
{
|
||||
return k.fee + outputs.size() * k.price_per_output + calculate_data_fee( fc::raw::pack_size(*this), k.price_per_kb );;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
} } // graphene::chain
|
||||
|
|
@ -192,7 +192,7 @@ void database_fixture::verify_asset_supplies( const database& db )
|
|||
}
|
||||
|
||||
BOOST_CHECK_EQUAL( core_in_orders.value , reported_core_in_orders.value );
|
||||
BOOST_CHECK_EQUAL( total_balances[asset_id_type()].value , core_asset_data.current_supply.value );
|
||||
BOOST_CHECK_EQUAL( total_balances[asset_id_type()].value , core_asset_data.current_supply.value - core_asset_data.confidential_supply.value);
|
||||
// wlog("*** End asset supply verification ***");
|
||||
}
|
||||
|
||||
|
|
|
|||
129
tests/tests/confidential_tests.cpp
Normal file
129
tests/tests/confidential_tests.cpp
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
/*
|
||||
* Copyright (c) 2015, Cryptonomex, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is provided for evaluation in private test networks only, until September 8, 2015. After this date, this license expires and
|
||||
* the code may not be used, modified or distributed for any purpose. Redistribution and use in source and binary forms, with or without modification,
|
||||
* are permitted until September 8, 2015, provided that the following conditions are met:
|
||||
*
|
||||
* 1. The code and/or derivative works are used only for private test networks consisting of no more than 10 P2P nodes.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
||||
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
|
||||
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
|
||||
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
|
||||
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
#include <boost/test/unit_test.hpp>
|
||||
|
||||
#include <graphene/chain/database.hpp>
|
||||
#include <graphene/chain/protocol/protocol.hpp>
|
||||
#include <graphene/chain/exceptions.hpp>
|
||||
|
||||
#include <graphene/chain/account_object.hpp>
|
||||
#include <graphene/chain/asset_object.hpp>
|
||||
#include <graphene/chain/committee_member_object.hpp>
|
||||
#include <graphene/chain/proposal_object.hpp>
|
||||
|
||||
#include <graphene/db/simple_index.hpp>
|
||||
|
||||
#include <fc/crypto/digest.hpp>
|
||||
#include "../common/database_fixture.hpp"
|
||||
|
||||
using namespace graphene::chain;
|
||||
|
||||
BOOST_FIXTURE_TEST_SUITE( confidential_tests, database_fixture )
|
||||
BOOST_AUTO_TEST_CASE( confidential_test )
|
||||
{ try {
|
||||
ACTORS( (dan)(nathan) )
|
||||
const asset_object& core = asset_id_type()(db);
|
||||
|
||||
transfer(account_id_type()(db), dan, core.amount(1000000));
|
||||
|
||||
transfer_to_blind_operation to_blind;
|
||||
to_blind.amount = core.amount(1000);
|
||||
to_blind.from = dan.id;
|
||||
|
||||
auto owner1_key = fc::ecc::private_key::generate();
|
||||
auto owner1_pub = owner1_key.get_public_key();
|
||||
auto owner2_key = fc::ecc::private_key::generate();
|
||||
auto owner2_pub = owner2_key.get_public_key();
|
||||
|
||||
blind_output out1, out2;
|
||||
out1.owner = authority( 1, public_key_type(owner1_pub), 1 );
|
||||
out2.owner = authority( 1, public_key_type(owner2_pub), 1 );
|
||||
|
||||
|
||||
auto InB1 = fc::sha256::hash("InB1");
|
||||
auto InB2 = fc::sha256::hash("InB2");
|
||||
auto OutB = fc::sha256::hash("InB2");
|
||||
auto nonce1 = fc::sha256::hash("nonce");
|
||||
auto nonce2 = fc::sha256::hash("nonce2");
|
||||
|
||||
out1.commitment = fc::ecc::blind(InB1,250);
|
||||
out1.range_proof = fc::ecc::range_proof_sign( 0, out1.commitment, InB1, nonce1, 0, 0, 250 );
|
||||
|
||||
out2.commitment = fc::ecc::blind(InB2,750);
|
||||
out2.range_proof = fc::ecc::range_proof_sign( 0, out2.commitment, InB1, nonce2, 0, 0, 750 );
|
||||
|
||||
to_blind.blinding_factor = fc::ecc::blind_sum( {InB1,InB2}, 2 );
|
||||
to_blind.outputs = {out2,out1};
|
||||
|
||||
trx.operations = {to_blind};
|
||||
trx.sign( dan_private_key );
|
||||
db.push_transaction(trx);
|
||||
trx.signatures.clear();
|
||||
|
||||
BOOST_TEST_MESSAGE( "Transfering from blind to blind with change address" );
|
||||
auto Out3B = fc::sha256::hash("Out3B");
|
||||
auto Out4B = fc::ecc::blind_sum( {InB2,Out3B}, 1 ); // add InB2 - Out3b
|
||||
blind_output out3, out4;
|
||||
out3.commitment = fc::ecc::blind(Out3B,300);
|
||||
out3.range_proof = fc::ecc::range_proof_sign( 0, out3.commitment, InB1, nonce1, 0, 0, 300 );
|
||||
out4.commitment = fc::ecc::blind(Out4B,750-300-10);
|
||||
out4.range_proof = fc::ecc::range_proof_sign( 0, out3.commitment, InB1, nonce1, 0, 0, 750-300-10 );
|
||||
|
||||
|
||||
blind_transfer_operation blind_tr;
|
||||
blind_tr.fee = core.amount(10);
|
||||
blind_tr.inputs.push_back( {out2.commitment, out2.owner} );
|
||||
blind_tr.outputs = {out3,out4};
|
||||
blind_tr.validate();
|
||||
trx.operations = {blind_tr};
|
||||
trx.sign( owner2_key );
|
||||
db.push_transaction(trx);
|
||||
|
||||
BOOST_TEST_MESSAGE( "Attempting to double spend the same commitments" );
|
||||
blind_tr.fee = core.amount(11);
|
||||
|
||||
Out4B = fc::ecc::blind_sum( {InB2,Out3B}, 1 ); // add InB2 - Out3b
|
||||
out4.commitment = fc::ecc::blind(Out4B,750-300-11);
|
||||
auto out4_amount = 750-300-10;
|
||||
out4.range_proof = fc::ecc::range_proof_sign( 0, out3.commitment, InB1, nonce1, 0, 0, 750-300-11 );
|
||||
blind_tr.outputs = {out4,out3};
|
||||
trx.operations = {blind_tr};
|
||||
BOOST_REQUIRE_THROW( db.push_transaction(trx, ~0), graphene::chain::blind_transfer_unknown_commitment );
|
||||
|
||||
|
||||
BOOST_TEST_MESSAGE( "Transfering from blind to nathan public" );
|
||||
out4.commitment = fc::ecc::blind(Out4B,750-300-10);
|
||||
|
||||
transfer_from_blind_operation from_blind;
|
||||
from_blind.fee = core.amount(10);
|
||||
from_blind.to = nathan.id;
|
||||
from_blind.amount = core.amount( out4_amount - 10 );
|
||||
from_blind.blinding_factor = Out4B;
|
||||
from_blind.inputs.push_back( {out4.commitment, out4.owner} );
|
||||
trx.operations = {from_blind};
|
||||
trx.signatures.clear();
|
||||
db.push_transaction(trx);
|
||||
|
||||
BOOST_REQUIRE_EQUAL( get_balance( nathan, core ), 750-300-10-10 );
|
||||
|
||||
} FC_LOG_AND_RETHROW() }
|
||||
|
||||
|
||||
|
||||
BOOST_AUTO_TEST_SUITE_END()
|
||||
Loading…
Reference in a new issue