Eosio.wrap (eosio.sudo) demystified

in #eos3 years ago (edited)

The eosio.wrap (formerly eosio.sudo) contract proposal and implementation has been a source of a lot of confusion and FUD in the past couple months. This post will walk through the code from the Block.one eosio.sudo contract, explain how it works, and address potential security concerns.

Brief FAQ

Here's a quick overview of eosio.wrap before diving into the code.

What is the relation between eosio.wrap and eosio.sudo?

eosio.wrap is the system privileged account under which the eosio.sudo contract will be deployed. So eosio.wrap refers to the EOS mainnet account that implements eosio.sudo, while eosio.sudo refers to the code. The decision was made to rename eosio.sudo to eosio.wrap for the mainnet because the contract does not contain potential security flaws such as retroactive write ability that exist in a typical sudo implementation or provide additional superuser capabilities that are not typically available to block producers. All it does is wrap an existing functionality for readability and transparency, so the new name more accurately reflects how it operates.

Why do we need eosio.wrap?

eosio.wrap is a contract that simplifies Block Producer superuser actions by making them more readable and easier to audit. It does not grant block producers any additional powers that do not already exist within the system. Currently, 15/21 block producers can already change an account's keys or modify an account's contract at the request of ECAF or an account's owner. However, the current method is opaque and leaves undesirable side effects on specific system accounts. eosio.wrap allows for a cleaner method of implementing these important governance actions.

Code Review

eosio.wrap will be a system contract with privileged authority and wide-reaching power. As such, it is important that block producers understand the code before agreeing to its implementation. Since the code was developed under the name eosio.sudo and still exists under that name, we will be using eosio.sudo interchangeably to refer to eosio.wrap in the code review.

Header File

Smart contract header files are generally where contract functions and tables are defined. The eosio.sudoheader file is printed below:

#include <eosiolib/eosio.hpp>

namespace eosio {

   class sudo : public contract {
      public:
         sudo( account_name self ):contract(self){}

         void exec();

   };

} /// namespace eosio

What's curious about the file is not what it contains, but what it doesn't contain. The vast majority of the header file is standard contract boilerplate that is required in every contract. The only real line of code in the whole header file is this:

void exec();

This defines a single contract function, exec, that takes no explicit arguments. As we will see later, the function actually does take input data, but it reads the data using low-level EOSIO methods instead of high-level function arguments.

Importantly, the contract contains no database tables. The contract does not and cannot maintain state across actions, which means any authorizations it is granted for an action are limited to that action only. Unlike a typical sudo authorization in command line, eosio.sudo does not grant retroactive write privileges for a specified time period. This is one of the reasons we have proposed calling the contract eosio.wrap instead of eosio.sudo.

ABI

The ABI file is far more detailed than the header file. It contains specifics on exactly what data types need to be included in the function arguments to execute a sudo action successfully. Full details of the ABI are not required to understand the contract, so only necessary highlights will be described here. The full file is here if you are interested.

ABI files define actions and tables that are publicly accessible to client software. The structs and types sections are helpers used to describe the data formats of the tables and actions.

An important security note: ABI enforcement can be bypassed when executing transactions. Messages and actions passed to a contract do not have to conform to the ABI. The ABI is a guide, not a gatekeeper, as multiple prominent hacks have demonstrated.

The true gatekeeper of a contract is the C-level apply function required in every contract. To avoid inserting difficult to read code with potential security holes into a contract, most developers use the EOSIO_ABI macro to wrap their apply function. We will see this pop up again when reviewing the contract code.

As in the header file, the ABI contains one action and no database tables:

  "actions": [{
      "name": "exec",
      "type": "exec",
      "ricardian_contract": ""
    }
  ],
  "tables": [],

More detail about the structure of the exec function is provided in the exec struct.

"name": "exec",
"base": "",
"fields": [
    {"name":"executer", "type":"account_name"},
    {"name":"trx", "type":"transaction"}
]

Executing a function requires passing in an account, the executer, that will be used to pay the RAM and CPU fees, and a raw packed transaction.

If you look deeper into the ABI, you'll see struct definitions for transaction and its sub-types transaction_header, action, permission_level, and action. These struct definitions make it seem like constructing the transaction portion is difficult, but in reality all it requires is passing in a standard EOS transaction JSON.

Creating a transaction JSON is supported by cleos with the -s -j -d flags (skip signature, json output, don't broadcast). For example, the command

cleos set account permission -s -j -d kedartheiyer active EOS63QUijU5kuaeQ8d4GnVekisWWKjZ4XkoGxFRyhEVZbc1hndK8u

would output the following transaction JSON:

{
  "expiration": "2018-10-02T14:09:27",
  "ref_block_num": 26989,
  "ref_block_prefix": 3356355165,
  "max_net_usage_words": 0,
  "max_cpu_usage_ms": 0,
  "delay_sec": 0,
  "context_free_actions": [],
  "actions": [{
      "account": "eosio",
      "name": "updateauth",
      "authorization": [{
          "actor": "kedartheiyer",
          "permission": "active"
        }
      ],
      "data": "709577aae56b928200000000a8ed32320000000080ab26a70100000001000297f0db93cecf32d34b739a9735236f2cbc65f3bb2b4d8471b2746994f86668df01000000"
    }
  ],
  "transaction_extensions": [],
  "signatures": [],
  "context_free_data": []
}

This transaction JSON could now be used by block producers to construct a sudo action.

The exec function

The full source for the eosio.sudo main file is located here.

We will walk through the exec function in chunks and explain each bit as we go.

The first line ensures that the authority for the account has been satisfied before attempting an execution.

void sudo::exec() {
   require_auth( _self );

On the mainnet, eosio.wrap will be controlled by eosio.prods, so attempting to execute an action without the approval of 15/21 active block producers will immediately fail.

The second block of code reads the action data into a memory buffer for later use.

constexpr size_t max_stack_buffer_size = 512;
size_t size = action_data_size();
char* buffer = (char*)( max_stack_buffer_size < size ? malloc(size) : alloca(size) );
read_action_data( buffer, size );

The EOSIO library provides 2 low-level functions to read raw action data: action_data_size and read_action_data. The raw action data is the byte representation of the arguments passed to an action. Transaction JSONs include hex representations of the raw action data in the data field of each action. For the transaction JSON we constructed in the previous section, the raw action data has the hex value 709577aae56b928200000000a8ed32320000000080ab26a70100000001000297f0db93cecf32d34b739a9735236f2cbc65f3bb2b4d8471b2746994f86668df01000000

The first 3 lines use the size of the action data to decide whether memory for the buffer should be allocated in the heap or the stack. Stack memory is cleared at the end of the function's execution, while heap memory in EOS is automatically freed at the end of an action. Confused readers can safely ignore this snippet with the assurance that no memory leaks will occur as a result.

Once the location and size of the buffer is determined, the 4th line reads the raw action data into the buffer for later use.

The next block of code uses the buffer to determine which account will pay for the wrapped transaction's RAM, NET, and CPU.

account_name executer;

datastream<const char*> ds( buffer, size );
ds >> executer;

As you may recall, the exec function takes 2 ABI arguments, an executerand a transaction. Raw action data is nothing but a tightly packed series of ABI arguments, so the first 8 bytes of the raw action data contains the executer (account_name is an alias for uint64_t), while the remainder contains the transaction.

A datastream is an efficient way of reading data from a buffer. ds >> executer reads the first 8 bytes of the buffer into the executer variable then moves the stream pointer to the next unread byte where the transaction argument begins.

The next line of code verifies that the executor has signed the transaction.

require_auth( executer );

This prevents an unwitting executor from accidentally having to pay the network costs for a transaction. Generally the proposer of a sudo transaction should set themselves as the executor.

The last 2 lines of code create a deferred transaction with no delay from the wrapped transaction. A deferred transaction is required so that

a) eosio.wrap can avoid paying the network costs for the wrapped transaction
b) the success and failure of the wrapped transaction can be isolated from the wrapper

size_t trx_pos = ds.tellp();
send_deferred( (uint128_t(executer) << 64) | current_time(), executer, buffer+trx_pos, size-trx_pos ); 

send_deferred takes a 128-bit ID, an executor, a pointer to a transaction buffer, and the transaction size in bytes as arguments.

The details of (uint128_t(executer) << 64) | current_time() aren't important. All you need to know is that it creates a unique 128-bit ID that can be used to cancel the transaction later if necessary.

tellp is used to determine the current position of the stream pointer, so trx_poswill contain the offset in bytes of the start of the transaction.

Deploying eosio.wrap

Implementing eosio.wrap requires 15/21 BPs to approve two separate proposals. One to create the eosio.wrap account, another to deploy the eosio.sudo contract to eosio.wrap. The first transation has to be executed before the second can be proposed.

Create eosio.wrap Account

You can review the first createwrap transaction with:

cleos multisig review libertyblock createwrap

This transaction contains 4 actions:

  1. eosio::newaccount: Create the eosio.wrap account
  2. eosio::buyrambytes: Buy 50 kB worth of RAM for the account. The EOS for the RAM purchase comes from the eosio account, which currently has an adequate balance of 12 EOS.
  3. eosio::delegatebw: Stake 1 EOS for NET and 1 EOS for CPU for eosio.wrap. This isn't very important because we can always delegate more resources to the account later if needed.
  4. eosio::setpriv: Make eosio.wrap a privileged account.

Deploy eosio.sudo contract to eosio.wrap

You can view the proposal to deploy the eosio.sudo code to the eosio.wrap account with:

cleos multisig review libertyblock deploywrap

There are 2 actions in the proposal:

1 . eosio::setcode: Set the code for the eosio.wrap account. The code hash after deploying will be 1a4d66fc40479949e47c517f057efd76f9861f7c6c6d4eeefaeb156866209d0a.

The code was compiled with the following compilation dependences:

The change-sudo-wrap branch has an open pull request to the eos-mainnet repo and will be merged in once the proposal has been approved.

2 . eosio::setabi: Set the ABI for the eosio.wrap account. The ABI used for this action can be found at https://github.com/EOSIO/eosio.contracts/blob/v1.3.1/eosio.sudo/abi/eosio.sudo.abi

If you want to run a test transaction on a localnet or testnet you can do so with the following cleos commands after deploying the contract. Swap out the test user accounts with valid account names on your network:

 cleos set account permission -s -j -d eptestusersa active EOS4uYQfroghuT2hGmdBofGNvJrygaFDHwQsT426fWAimtUhbcHJR owner > change.json
cleos wrap exec eptestusersb change.json

The sequence of actions and result should look like this:

Screenshot from 2018-10-09 07-41-24.png

You can see at the end that the active key for the account has changed to the one we specified.

Sort:  

Excellent write up. Do you have some examples of how this will be used?

I started a conversation back in July about this functionality, and I'm still not clear exactly how it would be used.

Currently, 15/21 block producers can already change an account's keys or modify an account's contract at the request of ECAF or an account's owner.

Has any this been done yet on EOS and are there people wanting something to be done like this now?

I wonder if we need to get our governance system in place first (referendum voting to approve a constitution which clarifies the role of arbitration, etc) before we make tools easier for BPs to use which have the potential of... well... messing something up. :)

Thoughts?

An example would be the EOS 911 accounts. Users that can prove with an Ethereum transaction that they lost their EOS key can request a key change from ECAF or directly from block producers. Many users have requested this specific service and my guess is this will become the first use case.

I see this as another tool just like the referendum that will be needed for governance. It doesn't add new functionalities, but simplifies an existing one and makes it more transparent. Every action actually taken with this tool will still have to be approved by BPs.

Thanks for the reply. It still seems a bit odd to me to build a tool to make a process easier when that process hasn't been used at all yet. Are there actions related to the EOS 911 accounts that are on pause right now, waiting for this? If not, then I guess my question still stands. It worries me a bit based on lack of voter engagement (75% of token holders still aren't voting) so that a small number of people with a large number of tokens could vote in BPs they control and use functionality like this to quickly (and easily) do bad things.

if somebody votes in 15/21 bad BPs and runs an attack on the network we are screwed no matter what system we have in place. There aren't any specific actions on hold because of this right now though, no. The most immediate use case would be for blacklisting keys, we could do so more effectively instead of the current fragile system.

Thanks for the reply. I agree, if someone was well-coordinated enough for an attack, they would most likely also be ready with signed transactions to do whatever it is they wanted to do (steal people's money, reset account keys, etc)

Could you elaborate a bit more on the better blacklisting solution? I've known the blacklist approach is temporary (and very fragile) and an all-or-nothing approach which doesn't really follow other DPoS 2/3+1 approaches. How would EOSIO.WRAP improve this?

Members of eosdacserver like myself have voiced our concerns, but we did go ahead and approve the proposal to create the account.

We could blacklist by changing their keys to unusable values. The account would be inaccessible to everyone, the action wouldn't require constant vigilance from all producers, and 15/21 producers would be able to execute the action.

Makes sense. Blowing away the keys on an account would definitely nuke it out. I imagine the blockchain world will go crazy the first time this happens. The real question is if we're ready for a truly governed blockchain.

Thanks for the replies.

Congratulations @blockliberty! You have completed the following achievement on the Steem blockchain and have been rewarded with new badge(s) :

Award for the total payout received

Click on the badge to view your Board of Honor.
If you no longer want to receive notifications, reply to this comment with the word STOP

Do not miss the last post from @steemitboard:

Presentamos el Ranking de SteemitBoard

Support SteemitBoard's project! Vote for its witness and get one more award!

Congratulations @blockliberty! You received a personal award!

Happy Birthday! - You are on the Steem blockchain for 1 year!

You can view your badges on your Steem Board and compare to others on the Steem Ranking

Do not miss the last post from @steemitboard:

New japanese speaking community Steem Meetup badge
Vote for @Steemitboard as a witness to get one more award and increased upvotes!