EOSIO Systems Design considerations when covering user resource costs

Published by jesta.x 20 Nov

EOSIO system design considerations when covering user resource costs

This goal of this article is to help outline design considerations while working with the ONLY_BILL_FIRST_AUTHORIZER feature. The article itself will be technical in nature and the target audience is both application developers seeking to cover resource costs for their users as well as service providers who wish to create new implementations similar to Greymass Fuel.

Terminology Reference

For those familiar with EOSIO, feel free to skip this section. However if you are unfamiliar with or new to EOSIO and the terminology typically associated with it, the following are brief definitions of some key terms used throughout the rest of this article.

  • Account: An entity representing a user of smart contracts on the network that has key pairs associated with it.

  • Action: An event that has been performed by an account on a smart contract.

  • Authorizing: The approval of an action by an account by adding a signature to a transaction.

  • Cosigner(s): The account(s) which create signatures for a transaction.

  • Key Pair: A public key and private key pair which can be used to generate and validate signatures.

  • Network: A blockchain network containing accounts and smart contracts.

  • Resource Costs: The CPU and NET resources an account incurs when performing actions on a smart contract.

  • Signature: The cryptographic signature(s) that can prove specific account(s) authorized actions within a transaction.

  • Smart Contract: A program which accounts can perform actions on.

  • Transaction: A term used to describe a group of one or more actions with the required signatures of the accounts involved.

What is ONLY_BILL_FIRST_AUTHORIZER (OBFA)?

ONLY_BILL_FIRST_AUTHORIZER, which I will also refer to as OBFA in the article, is the technical name of the feature in EOSIO 1.8 (PR#7089) that allows two parties to sign a single transaction, with the first party willing to assume the resource costs of the entire transaction.

The community typically refers to this as “the ability for dapps to pay resource costs for their users”.

What does OBFA change?

When OBFA is activated on an EOSIO-based blockchain, a global change takes effect on the blockchain which alters which accounts are billed for the resource costs of a transaction. By default on EOSIO (before OBFA activation), when a transaction is submitted all accounts involved in the authorization are billed equally for costs of the transaction. After the activation of OBFA, only the first authorizing account of the first action with an authorization of a transaction is billed for these costs.

OBFA is activated on a number of EOSIO-based blockchains already, including EOS.

Implementation

There are two system design topics we will focus on today, which I believe are largely undocumented. These two topics are:

  • Signature Collection Process

  • Transaction Schema Design

This post will not cover the actual process of generating signatures, integrating into specific applications, or any of the code or infrastructure needed to build out a full solution.

Signature Collection Process

Most EOSIO applications today operate under a simple model where each account assumes the costs for their own resource usage. To collect the appropriate signature from the user, an application proposes a transaction requiring the single signature, the user approves using their wallet, and the transaction is then published on the blockchain.

To put this process in a visual perspective, below is a simple flowchart outlining the process.

A standard process for an application to request a signature for a transaction.

The large boxes which encompass the various steps represent the different applications involved in this process. This standard process includes two components:

  • "The Application": an EOSIO-based application which runs on the users device. This could be a web, mobile, or desktop application that the user is using.

  • "User Wallet": an EOSIO-based key storage and signature provider which runs on a device owned and secured by the user. This could be a desktop, mobile, or hardware wallet.

The red diamond on the right, “Adds Signature”, is when/where the single signature required to authorize the transaction is generated.

This signature is created within the users wallet, external of the application itself, so the user doesn’t have to trust the application with their private keys. This practice of signature collection was established early on to prevent the security risks present anytime a private key is shared or potentially exposed.

With most applications operating under this model, how do they migrate to use OBFA?

Signature Collection under OBFA

Unlike the basic EOSIO application usage above, using OBFA to cover resource costs now requires a second signature on each transaction. The two signatures that need to be collected are:

  • The signature of the party who wants to perform an action (same as above).

  • The signature of the party willing to assume resource costs (new).

Both signatures are required to be a part of the signatures array that will be submitted with the transaction, otherwise the transaction will be rejected. To generate and collect both of these signatures, multiple approaches could be considered by an application:

  1. The user proposes and signs the transaction, which the application can sign and broadcast.

  2. The application proposes and signs the transaction, which the user can sign and broadcast.

  3. The application proposes the transaction, the user can sign, which can then be sent back and signed by the application operators through a service layer. It can then be broadcast to the network.

The end result of all these approaches is the same, with both parties creating a signature for the same series of actions within a transaction, and then the transaction is broadcast.

Each of these approaches however do have a variety of benefits and detriments. Lets take a look at each of these approaches.


1. User creates and signs first, then application…

This first approach has the most limited flexibility, use cases, and ends up creating significant friction in the user experience. Either the wallet itself or the user would have to initiate the transaction first, for the specific application, and then pass it to the application for the second signature.

The immediate problems with this approach make it a non-starter for most use cases. Unless its absolutely required, I would recommend against using this approach.


2. Application creates and signs first, then user…

In both the first and this second scenario, two components are involved in collecting and generating signatures:

  • The application

  • The user with a wallet

In this second scenario, the application is responsible for proposing and initially signing the transaction. Using a flowchart similar to the normal signature collection model, you can see a new red diamond added to the diagram below that shows the application creating the additional signature.

The process for an application to cosign a transaction before requesting the user sign.

Initially this approach make sense, since the application would only create transactions based on its specific design. It’s incredibly similar to how most applications operate without OBFA - the application just sends the transaction through a signing protocol, the user signs it, and done.

The problem with this approach surfaces as soon as you add the requirement for the application to generate a signature before passing it to the user - where do you keep the private key needed to generate the signature? How do you secure it? Anyone who gains access to this key could use it to perform any actions that are allowed by it.

In a world where single-page and downloadable applications make up the majority of EOSIO-based interactions, there’s no great answer to these questions regarding key security. The key should absolutely not be embedded within the application because there’s no way to secure it. For this reason, I would not recommend this approach.


3. Application creates, user signs first, then to a service layer…

The third scenario solves the apparent problems of the first two approaches by taking the original process and adding a third component into the mix.

This new component is a "Service Layer", running and secured on a server as a backend service invisible to the end user. While this model adds complexity to the overall architectural design of an EOSIO-based application, it allows for signatures to be created in a secure, non-distributed environment, completely outside of the application.

The following flowchart shows a process similar to that of non-OBFA applications (the top half), except the partially signed transaction is sent to the API, intercepted, analyzed, and then a signature is appended.

The process for requesting a transaction to be signed, then cosigning with a service layer.

The red diamond for adding the second signature now exists within this new "Service Layer", which solves the problem of key storage since the user has no access to the application running it. Along with being a solution to the previous approaches problems, this new layer also brings a number of new benefits:

  • Allows for controllable validation of the transactions before signing them, giving the application operators the final say in what gets signed.

  • The service layer can be updated independently of the application, in both its configuration and the private keys being used.

  • Additional features, such as quotas, whitelists, and filters can be established to prevent abuse of the resources being provided to users.

The only drawback we have found so far utilizing this solution is that now transactions will need to be submitted to specific API resources, as opposed to the first two approaches where transactions could be submitted to any API.

At this point, this is the solution I’d recommend for applications looking to cover the resource costs of their users. You can either build your own infrastructure and service layer, or work with a 3rd party provider of this service (such as Greymass Fuel) to mange it all for your application and users.

Transaction Schema Design

The second major topic to cover when working with OBFA is how you should craft the transactions themselves.

Instead of flowcharts, for this topic we will move into JSON representations of a transaction, with all examples provided being that of a user trying to vote for a proxy. The transaction we will be starting with in its full form is as follows.

{
  "expiration": "2019-11-09T04:03:48",
  "ref_block_num": 59228,
  "ref_block_prefix": 3880322651,
  "max_net_usage_words": 0,
  "max_cpu_usage_ms": 0,
  "delay_sec": 0,
  "context_free_actions": [],
  "actions": [
    {
      "account": "eosio",
      "name": "voteproducer",
      "authorization": [{
        "actor": "teamgreymass",
        "permission": "vote"
      }],
      "data": {
        "voter": "teamgreymass",
        "proxy": "greymassvote",
        "producers": []
      },
    }
  ],
  "transaction_extensions": []
}

To avoid repeating a lot of excess data, we will focus in on the actions portion of this transaction, and moving forward we will omit all of the transaction headers and other data normally associated with a transaction.

{
  "actions": [
    {
      "account": "eosio",
      "name": "voteproducer",
      "authorization": [{
        "actor": "teamgreymass",
        "permission": "vote"
      }],
      "data": {
        "voter": "teamgreymass",
        "proxy": "greymassvote",
        "producers": []
      },
    }
  ]
}

This is the action we are trying to perform, so what approaches can we take to utilize OBFA to cover the resource costs for the teamgreymass account during this process?

Adding an authorization to each action

The most straight forward approach to approving this transaction would be simply add an authorization for the account willing to cover the resource costs of the transaction.

Given that we have an account called cosigner with a permission of cosign, and the associated keys to generate a signature with it, we could just add an additional authorization like so as the first authorization:

{
  "actions": [
    {
      "account": "eosio",
      "name": "voteproducer",
      "authorization": [{
        "actor": "cosigner",
        "permission": "cosign"
      }, {
        "actor": "teamgreymass",
        "permission": "vote"
      }],
      "data": {
        "voter": "teamgreymass",
        "proxy": "greymassvote",
        "producers": []
      },
    }
  ]
}

In these actions, the cosigner@cosign authorization is the first authorization in the first action with an authorization in the transaction, and would be billed for the resource costs of the transaction. Both the cosigner@cosign and teamgreymass@vote would need to create a signature for this transaction in order to make it valid.

This approach however requires that the cosigner@cosign permission has the authority to perform the eosio:voteproducer action, and if it did not, the signature wouldn’t be valid.

Requiring the cosigner account to have actual permissions to perform the actions the cosignee wishes to perform then becomes a security risk, which would have to be mitigated by the application or service layer. The risks include:

  • An actor could try to swap the voter field from teamgreymass to cosigner, and cause the account meant for cosigning to actually cast the vote from the cosigner account.

  • If the service layer is compromised, and the key exposed, the actor with the key could now perform any actions the key was allowed to perform.

Both of these issues can be mitigated with proper security and a good service layer, but the design of the service layer ends up becoming more complex than it needs to be. For these reasons, I’d recommend against this approach and instead use the approach below.

Prepending a new Action

Instead of just adding authorities to the existing actions, another approach is to simply prepend a new action into the transaction, specifically to cover resource costs.

{
  "actions": [
    {
      "account": "greymassnoop",
      "name": "noop",
      "authorization": [{
        "actor": "cosigner",
        "permission": "cosign"
      }],
      "data": {},
    },
    {
      "account": "eosio",
      "name": "voteproducer",
      "authorization": [{
        "actor": "teamgreymass",
        "permission": "vote"
      }],
      "data": {
        "voter": "teamgreymass",
        "proxy": "greymassvote",
        "producers": []
      },
    }
  ]
}

In this series of actions, the cosigner@cosign authorization is still the first authorization in the first action with an authorization in the transaction, and will cover the resource costs for the rest of the actions within the transaction.

The first action is a NOOP, with the authorization of cosigner@cosign and no authorization from teamgreymass@vote. The second action is the vote we originally were trying to cast, only having the teamgreymass@vote authorization and not that of the cosigner@cosign.

This now means that the cosigner@cosign permission ONLY needs the authority to perform the greymassnoop:noop action, and no longer needs the authority to perform the eosio:voteproducer action (or any other action).

This solves the issues with the first approach since:

  • the cosigner account can’t perform any actions except the greymassnoop:noop action. If the private key for this permission was exposed, the worst thing the actor could do is now use the resources of the account freely bypassing any restrictions on the API layer.

  • the cosigner account can no longer be swapped into the eosio:voteproducer action, both because it’s not an authorization on the action because it can’t even perform the eosio:voteproducer action.

The same is also now true for the teamgreymass@vote permission - it does not need to be able to perform greymassnoop:noop, only eosio:voteproducer.

The NOOP?

This additional action doesn’t (and arguably shouldn’t) have to perform any operations on the blockchain - it can just be a blank action with no WASM logic attached. It can/should be a NOOP operation, one which doesn’t actually do anything.

This first action doesn’t specifically have to be a NOOP operation, but to the savings on network resource costs makes it a logical choice. If additional logic was placed within the first action, the total billing cost of resources would increase, as it would have to perform those operations on every transaction submitted and cosigned for.

We have deployed a NOOP contract to greymassnoop account on the EOS network, which anyone is free to use. This account is deployed with a basic ABI and no WASM code, which makes it the most efficient in terms of performance for these types of cosigning actions.

Note on the security of a NOOP: It’s also important to note that just because a contract claims to be a NOOP, that doesn’t mean it specifically is and that the action won’t actually do things. For this reason, the user account should not be submitted as an authorization on the NOOP action, and the NOOP being used should be verified as a legit NOOP. For the greymassnoop account we plan to burn the keys for it in the near future, making it an immutable piece of code that provably does nothing which cannot be changed.

At this point in time, this NOOP method is the way I would recommend designing transactions to be capable of securely cosigning.

In closing…

It’s my hope this exploration of a few system design topics regarding OBFA will help shed some light on how best to build these systems out and integrate them throughout the EOSIO ecosystem.

We (Greymass) have been experimenting with these new features pretty heavily over the last month or two, and we are constantly learning new things along the way. It’s entirely possible we’ll find new and better approaches to these designs as time progresses, and we’ll attempt to publish those findings in a similar way.

If you’d like to engage with us, about this or anything else, we’d invite you to reach out through our telegram group, via Twitter, or an email to hello@greymass.com.

About the Author

My name is Aaron Cox, I also go by jesta, and I’m one of the founding members of Greymass (greymass.com).

Greymass is block producer on various EOSIO networks (including EOS, Wax, and Insights), self-owned, and is funded purely by the inflation we receive fulfilling this role. To support our work you can vote for our block producer account, teamgreymass, which directly impacts the amount of inflation we are allocated. Every vote matters and we truly appreciate the support that has been shown to our organization since the genesis of the EOS blockchain.

Endorse
80fe6444c7ff02c79226d52eaedb8227d23bab04f807ce908b18a38f9fc3d15e