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

Consensus upgrade to objectively freeze an account #6150

Closed
arhag opened this issue Oct 26, 2018 · 3 comments
Closed

Consensus upgrade to objectively freeze an account #6150

arhag opened this issue Oct 26, 2018 · 3 comments
Labels
CONSENSUS Introduces a change that may modify consensus protocol rules on an existing blockchain. SNAPSHOTS Requires introducing a new version for snapshots and/or ends support for old versions of snapshots

Comments

@arhag
Copy link
Contributor

arhag commented Oct 26, 2018

Background

Current behavior of EOSIO makes it difficult to sell an EOSIO account name. Loopholes make it dangerous for a buyer to complete an account sale without an escrow. A new eosio::invaliddefer action made possible by the consensus feature upgrade described in #6118 along with the existing eosio.msig::invalidate action make it possible to sell an account in one atomic transaction without any worries about the owner permission being reverted back to the seller.

However, maintaining ownership of the newly purchased account is not all that a new account buyer cares about. Carrying out the atomic transaction with four actions described at the end of #6118 would not change the active permission authority for example. This typically means that the original owner would still have the authority to transfer tokens, unstake tokens, etc. These are all things that can negatively impact the market value of the newly purchased account after the sale has already been completed.

The other permissions of the sold account could be changed/deleted in the same atomic transaction that carries out the sale. However, the exact permissions to consider and the actions to include will vary from account to account. Furthermore, the permissions that need to be considered could change at any moment prior to the point at which the atomic transaction carrying out the sale actually retires in a block. With control of the owner permission, the buyer can evaluate the state of the account after the sale is complete and do whatever is necessary to clean up the state of permissions of the account so that the old owner does not have any remaining control. But it takes time to carry this out, and in the mean time the old owner may have, for example, unstaked tokens and transferred them to another account.

What would be helpful for the account buyer is to put the account into a state of being frozen in the same atomic transaction that carries out the sale. Then they could have plenty of time to make further transactions to clean up the account, all the while maintaining the invariant that the account remains frozen outside of these clean up transactions. When they are done, they could then unfreeze the account and begin using it normally. Such a freezing feature could in fact allow standardizing the atomic transaction that carries out the account sale to the point where it would be trivial to even implement them in smart contracts.

Account buyers should still be aware that even with an action to freeze an account (along with eosio::invaliddefer and eosio.msig::invalidate), they may still not be fully protected from an unexpected change in the property rights of the sold account from the time they do the final evaluation of the market value account and sign the sale transaction to the time when that sale transaction finally retires. For example, the seller may transfer all the liquid balances just before the sale transaction retires, leaving the new account owner in control of less tokens than they paid for. The solution to issues like this is to use an escrow contract (more details are provided for how this could potentially work in a later section of this document) and require the buyer (or an auditor they trust) to evaluate the state of the accounts assets and liabilities while it is in escrow before completing the sale.

A feature to freeze and unfreeze accounts in an objective way that the protocol respects could also make it much simpler for block producers to carry out arbitration orders that require (temporarily) freezing accounts. In a similar way to how selling an account requires considering all the ways custom permissions could be used to damage the value of the sold account (when there is no easy freeze operation), the BPs carrying out a freeze order on an account (without a an easy freeze operation that was part of the protocol) would need replace all the custom permissions of the account to be frozen to not allow the owner of the account to do anything with it. And if the account needed to be unfrozen and reverted back to its original state, the BPs would have to restore the authorities of all of the custom permissions. It would be much easier if there was just one action that could be used to safely (and objectively) freeze an account and another action to unfreeze the account and return it back to the same state before it was frozen.

Consensus upgrade feature

The goal of this consensus upgrade feature is to add new privileged intrinsics to objectively freeze and unfreeze any account and get the freeze status of any account.

A new consensus protocol upgrade feature will be added to trigger the changes described in this consensus upgrade proposal. The actual digest for the feature understood at the blockchain level is to be determined. For the purposes of this proposal the codename FREEZE_ACCOUNT will be use to stand-in for whatever the feature identifier will actually end up being.

This consensus upgrade feature will require a new field flags (likely to be a uint64_t) to be added to account_object (or more likely account_sequence_object, perhaps to be renamed, to be efficient with undo state size). The extra (up to) 8 bytes of overhead per account should be small enough to not require changing how much fixed RAM overhead each account is already charged. In fact, if the privileged field of bool type in account_object is replaced with a uint8_t type to be used as the new flags field, then no new memory overhead is required at all since the privileged boolean and the frozen boolean can both be accommodated with just 2 bits of the uint8_t flags field.

A new chain_snapshot_header version is necessary (e.g. version 2) to handle the changes of this consensus upgrade feature. Version 2 of the snapshot will include the new flags field in the rows of the corresponding affected index. The new version of nodeos supporting the FREEZE_ACCOUNT consensus upgrade feature should not be able to read a snapshot with a chain_snapshot_header version of 1 (this forces the necessary replay from genesis to occur). (Alternatively, nodeos could support reading version 1 snapshots and simply set the flags field to its default of 0 when loading from a snapshot.) The new version of nodeos should support version 2 of the snapshot when reading or writing a snapshot.

New intrinsics set_frozen and is_frozen

A new intrinsic called set_frozen which takes an account name and a boolean and returns a void should be added to the privileged_api. A new intrinsic called is_frozen which takes an account name and returns a boolean should be added to the privileged_api.

Contracts should only be able to link with these intrinsics after FREEZE_ACCOUNT activation.

When the set_frozen( uint64_t accnt, bool freeze ) intrinsic is called, it asserts that accnt represents a valid name of an existing account and sets the freeze bit of the new flags field for the specified account to 1 if freeze is true or to 0 if freeze is false.

When the is_frozen( uint64_t accnt ) intrinsic is called, it asserts that accnt represents a valid name of an existing account and returns the freeze bit of the new flags field for the specified account.

Note that the remaining unused bits (up to 63 bits) of the new flags are reserved (and should be set to zero) for future use. (Once again, one of the remaining bits may potentially be used to more efficiently represent the privileged boolean instead.)

If an account accnt is frozen (i.e its freeze bit is 1), then the native actions eosio::setcode, eosio::setabi, eosio::updateauth, eosio::deleteauth, eosio::linkauth, and eosio::unlinkauth will fail if the account field of these actions is accnt.

Furthermore, any account accnt is added to a set to be processed at the end of the transaction if accnt is the receiver or an actor in any authorizations of non-context-free actions that were considered while accnt was frozen (consideration occurs for inline actions at the time they were sent and it occurs for non-inline actions just prior to execution of that action; note that the receivers of action notifications, whether they are inline actions or not, are considered just prior to execution of the notification handler). In addition, at the time of scheduling a delayed input transaction or a contract-generated transaction, the payer of the contract-generated transaction and all the code accounts of and actors in authorizations of the non-context-free actions in the transaction should be evaluated to see if any are frozen, and those accounts that are should also be added to the set. At the end of the transaction, if any of the accounts in the set are still frozen, the entire transaction is rejected.

Changes to native actions (eosio_contract.cpp)

  • Add assertion that context.act.data_as<setcode>().account is not frozen in apply_eosio_setcode.
  • Add assertion that context.act.data_as<setabi>().account is not frozen in apply_eosio_setabi.
  • Add assertion that context.act.data_as<updateauth>().account is not frozen in apply_eosio_updateauth.
  • Add assertion that context.act.data_as<deleteauth>().account is not frozen in apply_eosio_deleteauth.
  • Add assertion that context.act.data_as<linkauth>().account is not frozen in apply_eosio_linkauth.
  • Add assertion that context.act.data_as<unlinkauth>().account is not frozen in apply_eosio_unlinkauth.

None of the above added assertions need to be guarded by an if statement checking for FREEZE_ACCOUNT activation because it is not possible for any account to become frozen prior to FREEZE_ACCOUNT activation.

Changes to transaction_context

Add a set of account_names called validate_freeze to transaction_context.

In transaction_context::finalize, iterate through the accounts in validate_freeze and assert that they are all not frozen.

Add a helper function transaction_context::consider_for_freeze( account_name accnt ) and if accnt is not already in the validate_freeze, it checks if the account accnt is frozen and if so adds it to validate_freeze.

Migrate the function controller::validate_referenced_accounts( const transaction& trx )const from controller.cpp to transaction_context.cpp but also change the signature to transaction_context::validate_referenced_accounts( const transaction& trx, bool enforce_freeze = false ). The method implementation should be further modified such that if enforce_freeze is true, it:

  • calls transaction_context::consider_for_freeze on a.account within the for loop iterating over non-context-free actions;
  • and, calls transaction_context::consider_for_freeze on auth.actor within the for loop iterating over the authorizations of each non-context-free action.

In transaction_context::init_for_input_trx, replace the call control.validate_referenced_accounts(trx); with validate_referenced_accounts( trx, trx.delay_sec.value > 0 );.

Again, no guard conditional on FREEZE_ACCOUNT activation is necessary for the above changes.

Changes to apply_context

In apply_context::execute_inline, call transaction_context::consider_for_freeze on a.account and on each actor in a.authorizations where a is the provided action.

In apply_context::exec_one just after getting the account of the receiver, if !context_free && (recursion_depth == 0 || receiver != act.account) (i.e. it is one of the original non-context-free actions of the transaction or a notification handler for an action whether inline or not), call transaction_context::consider_for_freeze on receiver.

In apply_context::schedule_deferred_transaction, replace the call control.validate_referenced_accounts( trx ); with trx_context.validate_referenced_accounts( trx, true ); and also call transaction_context::consider_for_freeze on payer right after the validate_referenced_accounts call.

Again, no guard conditional on FREEZE_ACCOUNT activation is necessary for the above changes.

How the new intrinsics are intended to be used

A system contract upgrade after FREEZE_ACCOUNT activation could be deployed which introduces four new actions: privfreeze, privunfreeze, freeze, and unfreeze. The system contract would need to be modify tables so that it can fit 3 bits into some table row (either existing or new table) for each account. It is acceptable if not every account has the table row that contains these 3 bits; those accounts would just not have support for the freeze and unfreeze actions until they get the appropriate table row to host the 3 bits (note that privfreeze and privunfreeze need to always work for every account). When the table row containing these 3 bits is created, it is important to initialize them properly to not break the invariants of privfreeze and privunfreeze: the system contract should initialize the value of those 3 bits for account accnt to 4 if ::is_frozen( accnt.value ) returns true, otherwise it should initialize it to 0.

Assuming a helper function std::optional<uint8_t> get_frozen_bits( eosio::name ); was implemented in the system contract to retrieve the value in the 3 bits for the specified account if they were present and assuming another helper function void set_frozen_bits( eosio::name, uint8_t ); was implemented in the system contract to set the value of the 3 bits for the specified account, the implementations of the four actions could be:

  • For eosio::privfreeze( eosio::name accnt ):
    eosio::require_auth( get_self() );
    auto fb = get_frozen_bits( accnt );
    
    if( fb.has_value() && (*fb < 4) ) {
       set_frozen_bits( accnt, (*fb | 4) );
    }
    ::set_frozen( accnt.value, true );
    
  • For eosio::privunfreeze( eosio::name accnt ):
    eosio::require_auth( get_self() ); 
    auto fb = get_frozen_bits( accnt );
    
    uint8_t new_unprivileged_level = 0;
    if( fb.has_value() ) {
       new_unprivileged_level = (*fb & 3);
       if( *fb != new_unprivileged_level ) {
          set_frozen_bits( accnt, new_unprivileged_level );
       }
    }
    if( new_unprivileged_level == 0 ) {
       ::set_frozen( accnt.value, false );
    }
    
  • For eosio::freeze( eosio::name accnt, bool require_owner_to_unfreeze ):
    eosio::require_auth( accnt ); 
    auto fb = get_frozen_bits( accnt );
    eosio_assert( fb.has_value(), "account's tables must be fully setup to enable freeze action" );
    
    uint8_t level = *fb;
    uint8_t new_unprivileged_level = (require_owner_to_unfreeze ? 2 : 1);
    if( new_unprivileged_level > (level & 3) ) {  
       set_frozen_bits( accnt, ((level & 4) | new_unprivileged_level) );
    }
    ::set_frozen( accnt.value, true );
    
  • For eosio::unfreeze( eosio::name accnt, uint8_t flags ):
    eosio_assert( flags < 4, "invalid flags" );
    bool require_owner  = (flags & 1);
    bool force_unfreeze = (flags & 2);
    
    if( require_owner ) {
       eosio::require_auth( eosio::permission_level{accnt, "owner"_n} );
    } else {
       eosio::require_auth( accnt ); 
    }
    auto fb = get_frozen_bits( accnt );
    eosio_assert( fb.has_value(), "account's tables must be fully setup to enable unfreeze action" );
    
    uint8_t level = *fb;
    eosio_assert( !force_unfreeze || level < 4, "cannot force unfreezing account under privileged freeze" ); 
    eosio_assert( require_owner || (level & 3) < 2, "must set flag to require owner permission while unfreezing" );
    
    if( (level & 3) != 0 ) {
       set_frozen_bits( accnt, (level & 4) );
    }
    if( (level & 4) == 0 ) {
       ::set_frozen( accnt.value, false );
    }
    

With these new system contract actions along with the new eosio::invaliddefer action discussed in #6118 and the existing eosio.msig::invalidate action, it becomes possible to easily and safely† do things like sell accounts or freeze accounts.

For example, a relatively safe† way to carry out an account sale in just a single atomic transaction would be by including the following actions in the transaction:

  1. eosio.token::transfer to transfer the tokens to the account seller as compensation for selling the account.
  2. eosio::invaliddefer to invalidate all authorizations by the account-to-be-sold in any prior scheduled deferred transactions (whether delayed input transactions or contract-generated transactions).
  3. eosio.msig::invalidate to invalidate all approvals by the account-to-be-sold on any proposed multisig transactions.
  4. eosio::updateauth to change the authority of the owner permission of the account-to-be-sold.
  5. eosio::freeze (with the require_owner_to_unfreeze boolean set to true) to freeze the account-to-be-sold in a way that requires the owner permission of the account to unfreeze it.

As an another example, a safe way block producers could objectively freeze an account is with a transaction with just the following action:

  • eosio::privfreeze to freeze the account in a way that cannot be unfrozen without an eosio::privunfreeze.

The block producers could just as easily objectively unfreeze the account and return it back to its state prior to the freeze with a transaction with just the following action:

  • eosio::privunfreeze to remove any privileged freeze there may be on the account.

†A note on the safety of selling accounts:

Safety in this context means that the previous owner cannot take back control of any of the permissions of the account or use the account after the sale if the new owner of the account does nothing after the sale. It also means that the new owner of the account has a process available to, at their leisure, clean up the state of the account and ultimately unfreeze it in such a way that the old owner is not able to take back control of any of the permissions of the account or use the account even after the account is unfrozen without the consent of the new owner.

However, it does not mean that changes are prevented to the account even just prior to the sale transaction retiring, even if those changes happen after the account buyer has signed and broadcast the sale transaction. So the proper way of handling account sales is to use an escrow (which could just be a smart contract on the blockchain) which temporarily takes over exclusive control of the to-be-sold account's owner permission and freezes the account in a way that cannot be unfrozen without the owner permission of the account. The seller could request to have the to-be-sold account returned back to them from the escrow at any time, but doing so invalidates the offer ID of the previous sale offer thus protecting potential buyers from purchasing an account that was not frozen for the entire time they were assessing the market value of the to-be-sold account.

An example account-selling escrow contract that demonstrates how to sell accounts in a really safe manner

So, as an example to demonstrate how this might work, a simple account-selling escrow contract might be implemented with the following four customer-facing actions: offersale, retractsale, acceptbuyer, and completesale.

The offersale( eosio::name seller, eosio::name account_to_be_sold, eosio::authority seller_owner_authority, std::string info ) action would:

  1. require_auth( seller ); and require_auth( permission_level{ account_to_be_sold, "owner"_n } );

  2. Create a new table record (account_to_be_sold.value as the primary key) to store the terms of the sale (all the arguments passed into the offersale action other than info) and a uint64_t field called offer_id which is a global auto-incrementing number and an eosio::name field called accepted_buyer that is initialized to eosio::name{}.

  3. Send an eosio::updateauth inline action to change the owner authority of account_to_be_sold to one that is exclusively satisfied by the eosio.code virtual permission of the escrow contract account. (The account seller would be expected to already set up the owner authority of account_to_be_sold to be unilaterally satisfied by the escrow contract's eosio.code virtual permission prior to calling the offersale action.)

  4. Send an eosio::freeze( account_to_be_sold, true ) inline action to freeze the account_to_be_sold account in such a way that requires owner permission to unfreeze.

  5. The contract may also potentially send an inline context free action that includes the offer_id in the action data to act as an event that can assist light clients that wish to verify the legitimacy of the info message for any offer referred to by the offer_id.

The retractsale( eosio::name seller, eosio::name account_to_be_sold ) action would:

  1. require_auth( seller );

  2. Get the table record of the existing sale offer for account_to_be_sold and assert that the seller in that record is in fact seller.

  3. Send an eosio::unfreeze( account_to_be_sold, 1 ) inline action authorized with the owner permission of account_to_be_sold.

  4. Send an eosio::updateauth inline action to change the owner authority of account_to_be_sold to seller_owner_authority stored in the table record.

  5. Delete the found table record.

The acceptbuyer( eosio::name seller, eosio::name account_to_be_sold, eosio::name buyer ) action would:

  1. require_auth( seller );

  2. Get the table record of the existing sale offer for account_to_be_sold and assert that the seller in that record is in fact seller.

  3. Modify the table record by setting the accepted_buyer field of the table record to buyer.

The completesale( eosio::name buyer, eosio::name account_to_be_sold, uint64_t offer_id, eosio::authority new_owner_authority ) action would:

  1. require_auth( buyer );

  2. Get the table record of the existing sale offer for account_to_be_sold and assert that the offer ID in that record is in fact offer_id and that accepted_buyer == buyer (accepted_buyer is from the table record).

  3. Delete the found table record.

  4. Send an eosio::unfreeze( account_to_be_sold, 3 ) inline action authorized with the owner permission of account_to_be_sold.

  5. Send an eosio::invaliddefer( account_to_be_sold ) inline action with the owner permission of account_to_be_sold. This will invalidate all authorizations by account_to_be_sold in any prior scheduled deferred transactions (whether delayed input transactions or contract-generated transactions).

  6. Send an eosio.msig::invalidate( account_to_be_sold ) inline action with the owner permission of account_to_be_sold. This will invalidate all approvals by account_to_be_sold on any proposed multisig transactions.

  7. Send an eosio::updateauth inline action to change the owner authority of account_to_be_sold to new_owner_authority.

  8. Send an eosio::freeze( account_to_be_sold, true ) inline action to freeze the account_to_be_sold account in such a way that requires owner permission to unfreeze.

To show how this example account-selling escrow contract (assume the account name of the escrow contract is sellaccounts) might be used, considered a hypothetical scenario where Alice (owns account alice) is trying to sell her other account greatname to Bob (owns account bob). The sequence of operations would typically be:

  1. Alice adds the sellaccounts@eosio.code permission to the owner authority of the greatname account. She could use the handy new convenience sub-command (Add cleos helper command to add eosio.code to permission #6116) in cleos: cleos set account permission greatname owner sellaccounts --add-code -p greatname@owner.

  2. Alice broadcasts a signed transaction with the offersale( "alice", "greatname", /* the current owner authority of greatname */, "Willing to sell to anyone who pays $100 USD worth of TOK. Compensation with other tokens may be accepted. Contact 'alice' with offers. The 'acceptbuyer' action will only be approved with the 'alice@active' permission." ) action.

  3. Bob notices the sale offer on a convenient website along with its associated offer_id (let's say it happens to be 1234) and the useful info message. Bob looks up the table record using cleos get table sellaccounts sellaccounts offers -l 1 -L greatname (talking to a trusted nodeos) to confirm the values including the offer_id (verifying the info message using cleos without trusting the website could also be possible, but too involved for this example).

  4. Using trusted block explorers and/or other helpful websites (or just cleos talking to a trusted nodeos) Bob examines the assets and liabilities of the greatname account to determine what they are willing to pay for the account.

  5. Bob agrees that the greatname account is worth at least $100 USD. Assuming each TOK is worth $5 USD at the moment, Bob decides he is willing to pay 20.0000 TOK for the account. Bob broadcasts a signed transaction that proposes (with eosio.msig) and also approves (with the bob@active permission) a transaction containing the following actions:

    1. eosio.token::transfer( "bob", "alice", "20.0000 TOK", "payment to buy account 'greatname'" ) with the authorization of bob@active.

    2. sellaccounts::acceptbuyer( "alice", "greatname", "bob" ) with the authorization of alice@active.

    3. sellaccounts::completesale( "bob", "greatname", 1234, /* new owner authority for greatname account */ ) action with the authorization of bob@active.

    The proposed transaction would request authorizations from bob@active (which would already be provided with the eosio.msig::approve action included by Bob in the same transaction that made the proposal) and from alice@active. For the proposed transaction to be executed, Alice would need to approve with the alice@active permission.

  6. Alice sees the proposed transaction (likely after Bob contacts her to point out his offer in the proposed transaction). If she agrees that 20.0000 TOK is fair compensation for the account, she can broadcast a signed transaction that approves the proposal with her alice@active permission (note that this could potentially be dangerous until issue eosio.msig approve is subject to TOCTOU race condition eosio.contracts#87 is resolved) and then executes it. As soon as this transaction successfully retires, 20.0000 TOK will have moved from the bob account to the the alice account and Bob will have ownership control of the (still frozen) greatname account.

  7. However, Alice may not have liked Bob's offer, In fact she may want to take the offer down. Perhaps she wants to put up a new offer with a different info message (perhaps asking for a higher price). Maybe she decided she does not want to sell greatname after all. Or maybe she just needs to take the offer down temporarily just to unfreeze the account so she can use it to do something. Whatever the case may be, she would first need to use the retractsale( "alice", "greatname" ) action to retract the sale offer.

After successfully completing steps 1 through 6 and purchasing the account, Bob will want to unfreeze the greatname account. However, before that, he must make sure that the account is in a state that is safe to unfreeze. For example, at a minimum he will want to replace the active authority of the greatname account from the old authority that Alice likely still could control (if only the account was not frozen) to one that only Bob controls. An atomic transaction to safely do this would likely have the following actions:

  1. eosio::unfreeze( "greatname", 3 ) authorized with the owner permission of greatname.

  2. eosio::updateauth to change the active permission of greatname.

  3. eosio::freeze( "greatname", true ).

@arhag arhag added HARDFORK SNAPSHOTS Requires introducing a new version for snapshots and/or ends support for old versions of snapshots labels Oct 26, 2018
@arhag
Copy link
Contributor Author

arhag commented Dec 7, 2018

This issue depends on #6429 and #6437. It will also be setup by default to be a protocol feature requiring pre-activation, thus it also depends on #6431.

@arhag arhag added CONSENSUS Introduces a change that may modify consensus protocol rules on an existing blockchain. and removed HARDFORK labels Mar 22, 2019
@StarryJapanNight
Copy link

This is excellent proposal and very well described. My exchange dapp gets a lot of pushback for escrowing the accounts listed for sale, in a very similiar manner to which you describe. Hopefully, with the upcoming launch of 1.8 we can make some progress on this protocal feature in addition to #6118.

@aclark-b1
Copy link

In order to focus our efforts on issues that are currently creating difficulty for the community we are closing tickets that were created prior to the EOSIO 2.0 release. If you believe this issue is still relevant please feel free to reopen it or create a new one. Thank you for your continued support of EOSIO!

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
CONSENSUS Introduces a change that may modify consensus protocol rules on an existing blockchain. SNAPSHOTS Requires introducing a new version for snapshots and/or ends support for old versions of snapshots
Projects
None yet
Development

No branches or pull requests

3 participants