EOSCommunity.org Forums

Initial specification for the Resource Provider API endpoint

With a formal EEP draft for Resource Providers underway, I decided to take a bit of time today to write up a post containing the specification of the primary API we have designed, both to explain the process and start gathering feedback.

This post will be technical, so if you are a non-technical user, following may be difficult. The take away for you however should be that progress is being made on what is known as Resource Providers (like Fuel) to give both free and paid transactions to users in order to lower the barriers of difficulty in using EOS and EOSIO.

The path so far, brief history

In late 2019 when we launched Fuel as one of the first resource providers, the approach we took was to have apps modify their transaction and submit them to our API for an additional signature to cover the resource costs using ONLY_BILL_FIRST_AUTHORIZER. We are now referring to this method of providing resources as the “postflight” method, since the account performing the transaction was required to create their signature first and then submit it to our APIs to receive the second signature. We then we broadcast that transaction to the blockchain. The resource provider signature was applied post-user and post-request.

This approach in the beginning worked well, but created a number of limitations in both what the resource provider could do, as well undesirable effects on the user experience. The application had to create the transaction, relay it to the users wallet for signature, then broadcast it to one specific endpoint to also be signed by the Resource Provider. This ended up…

  • creating a situation where it relied one API endpoint to push transactions.
  • requiring the resource provider API responses to conform with the push transaction format.
  • requiring the wallets to understand that it was OK to partially sign a transaction (modifications to the authority provider).
  • returning confusing errors to the user if for whatever reason the resource provider refused to sign (missing authority of the provider).
  • not allowing any sort of option to pay a fee for the transaction.

All of these issues were resolved by changing the order in which signatures were collected, moving the resource provider signature creation before the user signature creation, turning this process into a “preflight” operation. This shift in approach requires more client side logic to validate the transaction, but opened the doors for a number of new features and a much smoother user experience.

Resource Provider API, Revision 0

Our Resource Provider, Fuel, is now serving both postflight and preflight types of requests, with the goal to shift the vast majority requests eventually over to the preflight method. This post won’t cover postflight requests any further and will instead focus on the preflight request methods which is what the specification will eventually describe.

While we continue to work on the formal specification of the entire provider flow, we wanted to get the first API definition out in the wild to both explain how to implement it and to start gathering feedback.

This API endpoint being described for the remainder of this post is:

/v1/resource_provider/request_transaction

We currently operate Fuel v2 using this API endpoint on our EOS, Jungle (testnet), Telos, and WAX API servers. This endpoint serves as a generic entry point into a resource provider and as the name suggests, requests a specific transaction and the resources to be covered for it.

While this is by no means comprehensive, the following diagram illustrates the process on how a resource provider works in a preflight environment.

The process itself can be described as follows:

  1. An application creates a transaction for use on the EOSIO blockchain.
  2. If the application wants the resource costs of the transaction covered, it submits the transaction to a v1/resource_provider/request_transaction API endpoint(s) of the Resource Provider of its choosing.
  3. A resource provider receives the request, and based on their internal business logic, will decide whether or not to cover the transaction, and whether or not the transaction requires a fee and the amount of that fee.
  4. If the resource provider will cover the transaction costs (fee or free), it will modify the transaction with the appropriate changes required to make it successful (a noop, transfer, etc), sign the transaction, and return it to the application.
  5. The application will receive the modified transaction and signature. It will then verify the information in this new transaction, then create and append either a signature of its own or relay it to an end user for signature.
  6. Once all signatures are collected, it is then broadcast to the blockchain.

At no point during this process is trust implied and steps can be taken by both parties to verify the integrity of the transaction during the flow. Ultimately when both parties sign the transaction, they have agreed upon a common set of actions to perform:

  • The application (or end user) gets to perform the transaction they wish.
  • The resource provider agreed to cover the costs of the transaction (potentially based on specific conditions).

Since this is all done off chain, at any point either the resource provider or application can choose to stop the process and proceed no further.

Implementation

With a high level overview out of the way, let’s dive straight into how communication with the Resource Provider API would work.

Making a Request to a Resource Provider

The v1/resource_provider/request_transaction endpoint accepts a number of variations of POST parameters in an attempt to meet the needs of as many clients as possible. The first of these parameters is the signer parameter. This is the authorization of the account requesting the signature. The second parameter required is the transaction itself, which can be submitted in one of three ways:

  • As a request parameter by providing an EOSIO Signing Request (ESR) payload.
  • As a transaction parameter by providing a completely deserialized transaction.
  • As a packedTransaction parameter by providing a packed transaction like eosjs would return.

An example of the format of each of these is as follows:

Example of a EOSIO Signing Request payload request

{
    "signer": {
        "actor": "<requesting account actor>",
        "permission": "<requesting account permission>"
    },
    "request": "<EOSIO Signing Request Payload>"
}

Example of a deserialized transaction request

{
    "signer": {
        "actor": "<requesting account actor>",
        "permission": "<requesting account permission>"
    },
    "transaction": {
        "expiration": "2021-03-15T17:41:00.000",
        "ref_block_num": 10252,
        "ref_block_prefix": 2061542081,
        "max_net_usage_words": 0,
        "max_cpu_usage_ms": 0,
        "delay_sec": 0,
        "context_free_actions": [],
        "actions": ["<action>"],
        "transaction_extensions": []
    }
}

Example of a packed transaction request

{
    "signer": {
        "actor": <requesting account actor>,
        "permission": <requesting account permission>
    },
    "packedTransaction": {
        "compression": 0,
        "packed_context_free_data": "<context free data>",
        "packed_trx": "<packed transaction>",
        "signatures": [],
    }
}

All of these methods should be understood by the Resource Provider API to allow for the type of requests best suited for the application making the request. These two formats are accessible from developers using both eosjs and our new @greymass/eosio library.

The Response from a Resource Provider

After a request is made to a resource provider, three potential types of responses will be returned:

  1. The request was rejected (code 400).
  2. The request was approved, and has returned a modified transaction and signature (code 200).
  3. The request was approved, but a payment is required to proceed, and has returned a modified transaction and signature (code 402).

The application making the request should be ready to handle all three of these situations. Each of these response types returns a HTTP code both in the header and in the response body for consumption, along with the data relevant to the type of response.

The response itself will always conform to the following format:

{
    "code": 200,
    "message": "An optional message for the user",
    "data": {},
}

The code field will dictate how the application should proceed and which other data to expect in the response.

Code: 200 (Approved)

When the application receives a 200 code, the request for the transaction has been approved and a signature has been provided. The data field on the response now contains a few new pieces of information:

{
    "code": 200,
    "data": {
        "request": ["transaction", "<transaction data>"],
        "signatures": ["<signature>"],
    }
}

The request field within data now contains a modified version of the transaction that was submitted. This modified transaction will include the necessary requirements to cover the resource costs of the transaction. This typically includes a “noop action” as the first action and the resource provider as the first authorization of that first action. A signature for this modified transaction is also included.

The application itself should verify that this modified transaction matches the requested transaction and that only the expected modifications have been made. They should not blindly trust the modified transaction from the resource provider. We have developed example code for this and will be including it within the specification and potentially as a standalone library.

With the modified transaction verified to match what is expected, the application can either create a signature themselves for this new transaction or relay the transaction to the end user for signature in their preferred wallet. Once the user has signed this modified transaction, it can be broadcast to the blockchain and the resource provider will cover the costs.

Code: 400 (Rejected)

When the application receives a 400 code, the request for the transaction has been rejected for some reason. The data field on the response should now contain a message explaining why.

{
    "code": 400,
    "message": "<a message about the rejection>",
    "data": {}
}

This isn’t an error exactly and shouldn’t always be displayed to the user and block the application from performing the transaction. It’s just a rejection notice from this specific resource provider that resources won’t be provided for this specific transaction. The resource provider could return a 400 error if:

  • The user has network resources of their own and doesn’t need to have their costs covered.
  • The resource provider offers a limited amount of free resources per account, and this user has exceeded their quota.
  • The transaction doesn’t qualify for resources under the rules defined by the resource provider.

If a resource provider returns a 400, it just means that the application needs to use another resource provider or just perform the transaction without a resource provider.

Code: 402 (Approved, Payment Required)

The final type of response is the 402 code, which is that a payment is required to complete the transaction. The resource provider has deemed that it cannot cover the costs for free (a 200 response) and that the user doesn’t have enough resources of their own to perform the transaction (a 400 response).

{
    "code": 402,
    "data": {
        "costs": {
            "cpu": "<the portion of cost for cpu>",
            "net": "<the portion of cost for net>",
            "ram": "<the portion of cost for ram>"
        },
        "fee": "<the total amount of the fee>",
        "request": ["transaction", "<transaction data>"],
        "signatures": ["<signature>"]
    }
}

The structure of this response is similar to that of a 200, with the exception that the costs and fee fields have been appended as convenient ways to access the data within the modified transaction.

The request field in these responses contains more modifications to the transaction than a 200 response now. The modified request itself will return the “noop action”, but will now also include an action to pay for the transaction (a transfer for example), as well as potentially an action to purchase RAM for the account making the request.

Again the application must verify. The new costs and fee fields should not be blindly trusted and the entire transaction returned in the request field should be verified to ensure the data returned in these fields is accurate.

Once the application has verified that the transaction is acceptable:

  • If an application making the request without user interaction, it can append its own signature to the transaction and broadcast it to the blockchain.
  • If an application is making the request on behalf of a user, it can relay the modified request to the users wallet for a signature, which when returned can be broadcast to the blockchain.

Next Steps

By no means is this post outlining all of the steps involved, it especially lacks in both business logic and client implementation.

The business logic for the resource provider itself will be up to the individual providers, deeming how and when they’ll allow the cosigning of transactions to assume responsibility for resource costs. This business logic for the Resource Provider will vary based on its purpose - depending on whether it’s a general purpose Resource Provider like we offer with Fuel or it’s an application specific Resource Provider that only signs specific kinds of transactions (deposits into an exchange for example). Regardless of the purpose though, the format of the data exchange should be uniform so we, as developers, don’t have to reinvent the wheel and reimplement our code every time we want to use a different service.

Client implementations also have a long ways to go before the user experience for everyone using this process is great. We have implemented all of this logic within our Anchor Wallet specific SDKs, but in reality it needs to be implemented at a higher level so that all wallets can utilize it. This is going to require a new standard library, one that will likely replace UAL, that developers can drop into their applications to take advantage of the features Resource Providers have to offer. We have started prototyping a new library to do just that, but need more time invested into it before anything is ready to share.

I’m sure there are many other aspects of this process that need to be more well defined as well, which we’ll discover as we continue to evolve what it means to both be a Resource Provider and what it means to access one.

Feedback

As I mentioned opening this post, we’re in the process of formally proposing most of these ideas as an EEP (EOSIO Enhancement Proposal) like we did with the EOSIO Signing Request (EEP-7). What would be helpful before hand is your thoughts, primarily on the API definitions and call structures, and what challenges you as a developer may face when using this system.

Let me know in the thread below! Looking forward to the discussions…

4 Likes

These functionally are really necessary, Thanks for supporting the EOS(EOSio)chain.:+1:

3 Likes