Eosio.wrap (eosio.sudo) demystified
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.sudo
header 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 executer
and 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_pos
will 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:
eosio::newaccount
: Create the eosio.wrap accounteosio::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.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.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:
- eosio.contracts v1.3.1 (any 1.2.x versions will work as well)
- eosio.cdt v1.2.1 (https://github.com/EOSIO/eosio.cdt/tree/v1.2.1)
- The
change-sudo-wrap
branch of the LibertyBlock eos repo (a fork of the eos-mainnet repo): https://github.com/LibertyBlock/eos/tree/change-sudo-wrap
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:
You can see at the end that the active key for the account has changed to the one we specified.
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.
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:
Congratulations @blockliberty! You received a personal award!
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:
Vote for @Steemitboard as a witness to get one more award and increased upvotes!