This feature would allow people to transfer STEEM and SBD while keeping the amounts transferred private.
@bytemaster Would likely be best as an optional feature, for future regulatory compliance in all jurisdictions, as well as remaining on the Apple app store's whitelist (they made Jaxx remove DASH support because they aren't a fan of anonymous transactions).
The feature will be purely optional and only keeps the amounts involved anonymous. The users who are transacting will be public.
See commit: 9c5b957c9e1fa376c7c4df2204fc7a0fe1961457 for basic implementation, testing required.
Nice. Not familiar with C++ but noted your comments at line 70 in steem_confidential.hpp. Good luck with testing.
The comments seem to be leftover from a previous implementation since they refer to stealth transfers and fees which I think no longer apply to Steem's implementation, correct?
I don't think this line does what you want it to do according to the comments above. I haven't checked, but I believe it can be considered sorted by that function even with adjacent duplicates. It is better to go with the commented out implementation.
Why is this field necessary? The client producing the operation/transaction could always modify the blinding factors of the outputs so that the implementation assumes some hardcoded blinding factor (it would make sense to choose a blinding factor of 0) for the public input amount when checking if the inputs and outputs balance. So for example, this line would be replaced by something like:
FC_ASSERT( fc::ecc::verify_sum( {}, out, -net_public ), "", ("net_public",net_public) );
I think blind_transfer_operation should validate the account name of to, but only if the to_public_amount is non-zero. If to_public_amount is zero, then it should require that to be the empty string. Also, in its evaluator, this line should only be evaluated if op.to_public_amount.amount > 0 (in other words, get rid of that line and replace this line with db().adjust_balance( db().get_account( op.to ), op.to_public_amount );). This way the client can leave to as the empty string if there are no public outputs for the blind transfer operation.
@bytemaster: I just realized something important this implementation is missing. It would be incredibly valuable to have blinded outputs that are held as part of the "savings account". "Withdrawing" those funds would have the same 3 day protection as current savings balances. It should however be possible to instantaneously merge blinded savings outputs together into a new blinded savings output.
Also, I really hope to see the alternative way I proposed to do SBD interest so that users do not have to forgo collecting interest on their SBD balances in order to get financial privacy. The alternative mechanism has many other benefits as well.
@arhag if a blinding factor of 0 can work in the "assumed" case, then I suppose we can remove it.
I am not sure if there is any cryptographic security implications by using that factor.
Good catch on the sort order.
I agree that adding support for blind savings may be a critical distinction from other cryptos with privacy.
To prevent abuse, we may need to limit the number of blind outputs per account per SP. An account that has run out of available blind outputs will need to merge some of their outputs to make room to receive more.
@bytemaster: I like the way you are designing the blinded savings balances. I just wanted to mention that my earlier comment about instantaneously merging a user's blinded savings balances together into a new blinded savings balance is not really (easily) doable since it could invalidate the guarantee that a user's savings balance is safe as long as they recover their hacked account in 3 days (though perhaps you already figured that out?). If it was allowed to merge savings balances instantly, a hacker that gets a victim's active keys and memo key could immediately destroy the victim's entire blinded savings by merging the blinded outputs into one in which the victim does not know the blinding factor to spend the balance. While the hacker would not profit from that directly, they could hold that secret blinding factor (which unlocks the victim's entire blinded savings) for ransom.
To avoid this kind of attack, any transfer that touches existing blinded savings balances needs to be delayed (and revertible) for 3 days, which your design does. Unfortunately, it means that if a user's entire savings balance were held as a single blinded output then they cannot have concurrent pending savings transfers. This can be avoided by keeping their savings balance split into multiple blinded outputs, but they are still limited by the existing outputs available. A more complicated solution would track an acyclic dependency graph and allow pending transfers (which create new pending blinded outputs) to depend on existing pending blinded outputs, and this could give users for more flexibility. But that likely isn't worth it, especially if people infrequently use savings withdrawals and even then mostly to just move it from their savings balance to checking balance within the same account.
However, I do have some suggested changes to the existing design. I don't think that the transfer_to_blind_operation is necessary. I would get rid of it and modify the blind_transfer_operation to be more general. See (partial) code changes below.
struct blind_transfer_operation : public base_operation
{
account_name_type from;
asset from_public_checking_amount;
asset from_public_savings_amount;
asset public_change_amount;
bool change_to_savings;
optional<fc::ecc::blind_factor_type> blinding_factor;
vector<fc::ecc::commitment_type> inputs; /// must belong to from
vector<blind_output> outputs; /// can belong to many different people
void validate()const;
void get_required_active_authorities( flat_set<account_name_type>& a )const
{
a.insert(from);
}
};
void blind_transfer_operation::validate()const
{ try {
validate_account_name( from );
FC_ASSERT( public_change_amount.symbol == STEEM_SYMBOL ||
public_change_amount.symbol == SBD_SYMBOL );
FC_ASSERT( public_change_amount.symbol == from_public_checking_amount.symbol );
FC_ASSERT( public_change_amount.symbol == from_public_savings_amount.symbol );
FC_ASSERT( public_change_amount.amount >= 0 );
FC_ASSERT( from_public_checking_amount.amount >= 0 );
FC_ASSERT( from_public_savings_amount.amount >= 0 );
const auto& in = inputs;
vector<commitment_type> out(outputs.size());
int64_t net_public = public_change_amount.amount.value
- from_public_checking_amount.amount.value
- from_public_savings_amount.amount.value;
if( in.size() == 0 )
FC_ASSERT( outputs.size() > 0, "Cannot have both no blinded inputs and no blinded outputs" );
/// by requiring all inputs to be sorted we also prevent duplicate commitments on the input
for( uint32_t i = 1; i < in.size(); ++i )
FC_ASSERT( in[i-1] < in[i] );
for( uint32_t i = 0; i < out.size(); ++i )
{
out[i] = outputs[i].commitment;
validate_account_name( outputs[i].owner );
FC_ASSERT( out[i].savings == blind_balance_object::checking || out[i].savings == blind_balance_object::savings );
if( i > 0 ) FC_ASSERT( out[i-1] < out[i] );
}
if( out.size() == 0 )
{
FC_ASSERT( blinding_factor, "Blinding factor must be specified if there are no blinded outputs");
auto public_c = fc::ecc::blind(*blinding_factor, net_public);
FC_ASSERT( fc::ecc::verify_sum( in, {public_c}, 0 ), "", ("net_public",net_public) );
}
else if( in.size() == 0 && out.size() == 1 )
{
if( blinding_factor )
{
auto public_c = fc::ecc::blind(*blinding_factor, -net_public);
FC_ASSERT( fc::ecc::verify_sum( {public_c}, out, 0 ), "", ("net_public",net_public) );
}
else
{
FC_ASSERT( fc::ecc::verify_sum( in, out, net_public ), "", ("net_public", net_public) );
}
}
else
{
FC_ASSERT( !blinding_factor,
"Blinding factor can only be specified if either there are no blinded outputs or there are no blinded inputs and exactly one blinded output");
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 <= STEEMIT_MAX_SHARE_SUPPLY );
}
}
} FC_CAPTURE_AND_RETHROW( (*this) ) }
void blind_transfer_evaluator::do_apply( const blind_transfer_operation& op ) {
const auto& from = db().get_account( op.from );
FC_ASSERT( db().get_balance( from, op.from_public_checking_amount.symbol ) >= op.from_public_checking_amount );
FC_ASSERT( db().get_savings_balance( from, op.from_public_savings_amount.symbol ) >= op.from_public_savings_amount );
uint8_t is_pending = (from_public_savings_amount.amount.value > 0) ? blind_balance_object::pending : 0;
for( const auto& in : op.inputs ) {
const auto& bbi = db().get<blind_balance_object,by_commitment>( in );
FC_ASSERT( bbi.owner == op.from );
FC_ASSERT( bbi.symbol == op.public_change_amount.symbol );
FC_ASSERT( !(bbi.state & blind_balance_object::pending) );
if( bbi.state & blind_balance_object::savings )
is_pending = blind_balance_object::pending;
}
for( const auto& in : op.inputs ) {
const auto& bbi = db().get<blind_balance_object,by_commitment>( in );
if( is_pending )
db().modify( bbi, [&]( blind_balance_object& bbo ) {
bbo.state |= is_pending;
});
else
db().remove( bbi );
}
for( const auto& out : op.outputs ) {
db().create<blind_balance_object>( [&]( blind_balance_object& bbo ) {
bbo.symbol = op.public_change_amount.symbol;
bbo.owner = out.owner;
bbo.commitment = out.commitment;
bbo.state = out.savings | is_pending;
#ifndef IS_LOW_MEM
bbo.confirmation.resize( fc::raw::pack_size( out.stealth_memo ) );
fc::datastream<char*> ds (bbo.confirmation.data(), bbo.confirmation.size() );
fc::raw::pack( ds, out.stealth_memo );
#endif
});
}
db().adjust_balance( from, -op.from_public_checking_amount );
db().adjust_savings_balance( from, -op.from_public_savings_amount );
if( is_pending ) {
db().create<pending_blind_transfer_object>( [&]( pending_blind_transfer_object& pbt ){
pbt.owner = op.from;
pbt.expiration = db().head_block_time() + fc::days(3);
pbt.pending_commitments.reserve( op.inputs.size() + op.outputs.size() );
for( const auto& in : op.inputs )
pbt.pending_commitments.push_back(in);
for( const auto& out : op.outputs )
pbt.pending_commitments.push_back(out.commitment);
pbt.from_public_checking_amount = op.from_public_checking_amount;
pbt.from_public_savings_amount = op.from_public_savings_amount;
pbt.public_change_amount = op.public_change_amount;
pbt.change_to_savings = op.change_to_savings;
});
}
else if( op.public_change_amount.amount > 0 )
{
if( op.change_to_savings )
db().adjust_savings_balance( from, op.public_change_amount );
else
db().adjust_balance( from, op.public_change_amount );
}
};
This allows that single operation to move purely public balances (checking and/or savings) to blinded balances (potentially with some public balance change output to either savings or checking), or move blinded balances (potentially with a checking and/or savings public balance input) to other blinded balances (potentially with some public balance change output to either savings or checking), or move blinded balances (potentially with a checking and/or savings public balance input) to a purely public balance to either savings or checking (this last transformation is something that the existing design would require including an extra operation that creates a temporary blinded output to then be immediately consumed by the blind_transfer_operation in order to make the input blinding factors sum to 0). But the main benefit to this design is that it allows a user to move their blinded savings balances to their public savings balance (after 3 day delay) or their public savings balance to their blinded savings balances (after 3 day delay) without ever having to expose those funds to the vulnerable checking balances.
Also, I think it is really important to figure out a good standard (and implement it in both cli_wallet and steemit.com) for securely (and efficiently) backing up the previous memo private keys on the blockchain any time the memo key is updated so that under normal circumstances the current memo private key can be used to recover all the previous memo private keys used with the account. We already need something like this to be able to recover all the old memos no longer accessible because of a changed memo key, but it becomes super important when losing those old keys can mean losing access to one's blinded funds. It would also be nice to backup encrypted data to the blockchain that allows the current memo private key to be recovered from the blockchain by knowing only owner private key. That way someone who has securely backed up owner private key can feel confident that they can (assuming not hacked) get access to all their funds with their owner private key alone. Of course, if they lose access to all of their keys, then even with the future owner authority reset feature, they will still lose access to all blinded funds (nothing can be done to help them in that case).
@arhag can you continue with this implementation and produce the unit tests to prove it works?
One thing I would like to maintain is simple "account" to "account" transfers where account == check or savings. Transactions that simultaneously withdraw from multiple accounts and/or deposit to multiple accounts are nearly impossible to display on a ledger.
Having users do "two steps" via "two operations in one transaction" is often cleaner than having one complex transaction that does everything.
allows a user to move their blinded savings balances to their public savings balance (after 3 day delay) or their public savings balance to their blinded savings balances (after 3 day delay) without ever having to expose those funds to the vulnerable checking balance
This is an important use case.
Transactions that simultaneously withdraw from multiple accounts and/or deposit to multiple accounts are nearly impossible to display on a ledger.
Suppose Alice sends Bob 100 STEEM from public balance, 200 STEEM from private balance, 300 STEEM from private savings. The UI might appear as:
txid From To Amount
1a15cef... alice (B+P+PS) bob 600.000 STEEM
Expanded view would be:
txid From To Amount
1a15cef... alice (B) bob 100.000 STEEM
1a15cef... alice (PB) bob 200.000 STEEM
1a15cef... alice (PS) bob 300.000 STEEM
Having users do "two steps" via "two operations in one transaction" is often cleaner than having one complex transaction that does everything.
This logic holds _when operations complete instantaneously_, but savings transfers don't. That means the time delay of savings transfers means you have to worry about atomicity and timing differences. If for example we require a pub_savings -> priv_savings transaction to go through a two-op sequence of (pub_savings -> pub_account, pub_account -> priv_savings) then:
@arhag Agree with redundancy of transfer_to_blind operation. However your design allows what I will call "heterogenous" public inputs, i.e. you can transfer from both savings _and_ checking in a single op. This function can be simulated by simply doing a regular checking -> savings deposit followed by a blind transfer from savings. I think we shouldn't do heterogenous public inputs / outputs.
Blind inputs/outputs should be able to be heterogenous, otherwise you would be unable to transfer part of a blind savings balance to blind checking.
So something interesting about blind savings balances. Any balance which has at least one savings input and at least one checking output must be delayed. (TODO: I need to verify that this is enforced in the code.) Such a transaction could be a deposit or withdrawal, whether the savings is net increase or decrease can't be determined unless you can unmask the blind balances.
This means that if the wallet is making a deposit to savings, this needs to be done in two-step to prevent a delay: First create a balance with exactly the right amount, secondly create a tx with only savings balance outputs.
When making a withdrawal from savings, making subsequent withdrawals from the same balance cannot be done because the withdrawals must be processed in some order. We can allow multiple pending withdrawals if we make the second withdrawal dependent on the first withdrawal completion by making the second spend outputs do not exist when the subsequent withdrawal is initiated, but come into existence when the first withdrawal completes. (Similarly we can make a second withdrawal dependent on the first withdrawal's failure by making it (double) spend the first withdrawal's output.)
The next point I need to address is that all actions which have a blind savings balance input must be deferred. We might imagine eliminating the delay for any transactions involving re-shuffling savings balances (without creating any new checking balances). However this cannot be done, because the threat model the savings account was designed for was an adversary who temporarily gains access to an account's key, but whose actions are noticed and suppressed by the account's legitimate owner within the transfer time window (3 days?). Allowing non-delayed re-shuffling of savings balances would allow the adversary in this threat model to destroy savings balances by re-shuffling them into balances with commitments the legitimate account owner does not know.
This leads us to a more strict criterion than the previous post, namely, that any transaction with a blind savings input needs to be delayed (even if it only has savings outputs).
Our library can produce shorter proofs if we are willing to use a more precise upper bound than STEEMIT_MAX_SHARE_SUPPLY (at the cost of leaking information). We can certainly bound each balance at the total amount of blind balances, so we should track that. We should track this in the core, so we can add a no-printing failsafe to the ops.
We can also do a more precise upper bound with some graph analysis. Basically if we track an upper bound on all inputs, then an upper bound for all outputs is the sum of upper bounds of all inputs (minus number of inputs minus one since we require all outputs to be nonzero, i.e. biggest possible output results when all inputs equal their upper bound and are concentrated into a single output, except for a single satoshi on all other outputs). This graph analysis should be a plugin, which can be used by the wallet to generate smaller proofs which still preserve privacy.
After some discussion with @bytemaster we've decided to move the consensus part of this from a state machine semantics to deferred-apply semantics, with an eye to keeping the consensus code as simple as possible.
For wallet advisory purposes (i.e. telling the user "if you cancel pending withdrawal X, then pending withdrawals Y and Z will fail"), we can create a plugin which tracks some extra information about output references.
I think we have a more general problem that needs to be addressed:
If the blockchain evaluation logic tracked the current "evaluation delay" as a blockchain parameter, then operations can verify that "evaluation delay" >= "required by operation".
Then we could have a single operation, lets call it "delayed evaluation" that contains a vector of other operations and a "delay time". On the first block after the "delay time" has passed the operations are applied after first setting the "evaluation delay" variable.
All operations applied from a signed_transaction are done so with "evaluation delay of 0", deferred operations can set a different evaluation delay prior to applying the ops.
With a few small changes, this process could also be used for "proposed transactions" like bitshares multisig.
Partial rewrite in branch 568-blind-rewrite (see above)