Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FLIP: FCL Tx Payer Service #612

Open
wants to merge 17 commits into
base: master
Choose a base branch
from
Open
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
349 changes: 349 additions & 0 deletions flips/20210824-fcl-tx-payer-service.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,349 @@
# FCL Transaction Payer Service

| Status | Proposed |
:-------------- |:---------------------------------------------------- |
| **FLIP #** | [612](https://github.com/onflow/flow/pull/612) |
| **Author(s)** | Jeffrey Doyle ([email protected]), |
| **Sponsor** | |
| **Updated** | 2020-09-09 |

## Objective

There is a need for the developement of a suite of tools that will collectively
act as a Transaction Fee Payer Service for use by applications built on Flow.

Technically, this suite of tools will facilitate signing as the payer of transactions initiated
by application's users with an account controlled by the application itself.

From a functional perspective, the service would enable users of applications that employ
these tools to be able to execute transactions, and thereby interact with Flow, without
encurring the burden of having to pay for their transaction fees themselves. This user experience
is valuable for many reasons, including enabling users to engauge with applications while not
having _any_ FLOW in their account, or having to worry about having _enough_ FLOW to execute the
transactions they wish to execute.

A Transaction Fee Payer Service would exist as a resusable codebase / set of
packages & tools that could be used by the applications that desire it.

## Motivation

One of the many features of Flow is that transaction fees can be paid by an account
separate from the authorizers of that transaction. With this feature, applications that wish to
provide "free" transactions for their users could opt to pay for their users'
transactions themselves. They would do this by producing a signature for the
transaction as the payer of that transaction. Upon receiving this transaction,
the Flow network would then deduct transaction fees from the payer, not any of
the authorizers.

For a user to pay their transaction fees themselves, that user would have to have FLOW
in their account. This can lead to a situation where a first-time Flow user attempts to engage with an application
that does not employ a Transaction Payer Service, and they would first need to acquire FLOW before engaging with
that application. This is an undesireable user experience, as it significantly raises
the friction required of first time Flow users to engauge with these applications. Many users who are unfamiliar
with blockchain applications will not understand or sympathize with this requirement. Thus it is crucial
that applications who wish to provide a low friction experience for their users to pay transaction fees on
their behalf.

Some FCL compatible wallets choose to act as the payer of the transactions their users
sign. They do this by swapping out their users account with an account of their own,
if their users are specified as the payer of a transaction. This allows wallets with
this feature to provide "free" transactions for their users. While this makes for a
desirable user experience for those that use wallets with this feature, applications do not
get the guarantee that _all_ of their users will get this user experience.
This is because users are able to use whichever FCL compatible wallet they desire with the FCL
compatible applications they use, and not all FCL compatible wallets will strictly choose to provide this feature.

Applications that wish to guarantee their users can submit transactions
without having to pay transaction fees for them will then have to pay those fees themselves.
Comment on lines +100 to +101
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Slightly confusing... how about:

Suggested change
Applications that wish to guarantee their users can submit transactions
without having to pay transaction fees for them will then have to pay those fees themselves.
Applications that wish to guarantee their users can submit transactions
without having to pay transaction fees will have to pay those fees in the users' stead.

To pay transaction fees, it's expected that applications would need to build a Transaction Payer Service that is able
to receive a transaction signable and then produce a signature for it.

A re-usable Transaction Payer Service would exist as project / set of packages and tools that applications could use
instead of having to build an implementation of them themselves. These re-usable components
would lower the overall complexity and effort required of a developer who wishes that
their application provide "free" transactions for their users.

## User Benefit

Most consumer applications that we interact with on a daily basis charge us nothing.
As internet users, we're freely able to interact for our favourite social media apps,
interact with our favourite websites and simply do as we please without paying for anything.
However, with blockchain applications, each one of those interactions would likely be
transaction, and with that, transaction fees would have to be paid.

Users have been conditioned to expect things for free. As such, applications that wish
to provide the best user experiences will likely choose to allow their users to execute
transactions without them having to pay.

This Transaction Fee Payer Service will enable more applications to offer a top of the line user experience, where users can easily and seamlessly engage in any action without the friction of having to pay
for any transaction fees.

## Design Proposal

> Disclaimer: The following is an example implementation for what _might_ work. All possible solutions are not limited to just the following. You're completely free and encouraged to approach the problem however you choose, even if it differs extensively from the thoughts below.

When creating a transasction to be sent to the blockchain, FCL expects authorization functions
for each of the roles for that transaction to be specified.

For example, when specifying to FCL that the "Current User" of the application
should be an Authorizer, the Proposer and the Payer of a transaction, it might
look something like:

```javascript
// IN CLIENT APPLICATIONS FRONTEND

import * as fcl from "@onflow/fcl"

const tx = await fcl.send([
fcl.transaction`... Cadence Code ...`,
fcl.authorizers([fcl.currentUser().authorization]),
fcl.proposer(fcl.currentUser().authorization),
fcl.payer(fcl.currentUser().authorization)
]).then(fcl.decode)
```

The line `fcl.currentUser().authorization` is an "Authorization Function".

(Read More about Authorization Functions: https://github.com/onflow/fcl-js/blob/master/packages/fcl/src/wallet-provider-spec/authorization-function.md)

The implementation of this Authorization Function is what FCL uses to gather a
signature from the Current User of the application for each of the roles of the
transaction (Proposer, Payer and Authorizer).

A Transaction Fee Payer Service would need to make available an Authorization Function
that applications could use in place of `fcl.currentUser().authorization` (or whatever other
authorization function they use).

The applications transaction code might then look something like:

```javascript
// IN CLIENT APPLICATIONS FRONTEND

// Note: "TPS" <=> "Transaction Payer Service"

import * as fcl from "@onflow/fcl"
import { TPSAuthorizationFunction } from "@TPS/TPS-client"

const configuredTPSAuthzFn = TPSAuthorizationFunction({
... TPS Configuration ...
resolveAccountURL: "https://api.myawesomedapp.com/tps/resolveaccount" // Example
signingURL: "https://api.myawesomedapp.com/tps/sign" // Example
})

const tx = await fcl.send([
fcl.transaction`... Cadence Code ...`,
fcl.authorizers([fcl.currentUser().authorization]),
fcl.proposer(fcl.currentUser().authorization),
fcl.payer(configuredTPSAuthzFn)
]).then(fcl.decode)
```

The implementation of `TPSAuthorizationFunction` will need to conform to how FCL expects
Authorization Functions to work. (Read More about Authorization Functions: https://github.com/onflow/fcl-js/blob/master/packages/fcl/src/wallet-provider-spec/authorization-function.md)

The implementation of the `TPSAuthorizationFunction` might look something like:

```javascript
// IN "@TPS/TPS-client"

export const TPSAuthorizationFunction = ({ resolveAccountURL, signingURL }) => async (account) => {

/**
Perform a network call to resolve the FCL "account" data structure. This network call will likely be to the client applcation's backend.

The call graph might look something like:

Client App TPSAuthorizationFunction =POST=> Client Backend =POST Response=> Client App TPSAuthorizationFunction

The TPS API would need to return back an account data structure containing:
(See: https://github.com/onflow/fcl-js/blob/master/packages/fcl/src/wallet-provider-spec/authorization-function.md#how-to-create-an-authorization-function for more information on the purpose of each field)
{
...account,
tempId: tpsTempID
addr: tpsPayerAddress
keyId: tpsPayerKeyID
}
**/
const resolvedAccount = await fetch(resolveAccountURL, {
method: "POST",
headers: { "Content-Type": "application/json" }
body: JSON.stringify(account)
}).then(res => res.json())

// The authorization function returns an FCL account data structure with more fields filled in.
return {
...resolvedAccount,
signingFunction: async signable => {
/**
Perform a network call to resolve the transaction signature. This network call will likely be to the client applcation's backend.

The call graph might look something like:

Client App TPSAuthorizationFunction =POST=> Client Backend =POST Response=> Client App TPSAuthorizationFunction

The TPS API would need to return back a composite signature data structure containing:
(See: https://github.com/onflow/fcl-js/blob/master/packages/fcl/src/wallet-provider-spec/authorization-function.md#how-to-create-an-authorization-function for more information on the purpose of each field)
{
addr: tpsSignerAddress
keyId: tpsSignerKeyID
signature: tpsSignature
}
**/
return await fetch(signingURL, {
method: "POST",
headers: { "Content-Type": "application/json" }
body: JSON.stringify(signable)
}).then(res => res.json())
}
}
}
```

The application would need to expose an API/webservice with POST routes for the
`resolveAccountURL` and `signingURL` URLs the `TPSAuthorizationFunction`
would use. The application would need to perform the actions of resolving the account,
or signing the signable for those routes respectively.

The Client Application's backend might look something like:

```javascript
// IN CLIENT APPLICATION BACKEND
import { TPSAccountResolver, TPSSigner } from "@TPS/TPS-server"

var express = require('express')
var app = express()

const TPSConfiguration = {
accounts: [
{
address: "0xABC123",
privateKey: process.env.ACCOUNT_ABC123_PRIVATE_KEY_HEX,
keyIndex: 0
},
{
address: "0x456DEF",
privateKey: process.env.ACCOUNT_456DEF_PRIVATE_KEY_HEX,
keyIndex: 0
}
],
}

const configuredTPSAccountResolver = await TPSAccountResolver(TPSConfiguration)
const configuredTPSSigner = await TPSSigner(TPSConfiguration)

app.post("/tps/resolveaccount", (req, res) => {
const account = req.body

const resolvedAccount = await configuredTPSAccountResolver(account)

res.send(resolvedAccount)
})

app.post("/tps/sign", (req, res) => {
const signable = req.body

const compositeSignature = await configuredTPSSigner(signable)

res.send(compositeSignature)
})
```

The applications backend might consume a package, `"@TPS/TPS-server"`, which would ideally expose some helpful utilities
for handling the logic behind resolving an account and producing a payer signature. The above code snippet uses these
potential utilities.

The implementation of these utilities, `TPSAccountResolver` and `TPSSigner` might look like:

```javascript
// IN @TPS/TPS-server

import {WalletUtils} from "@onflow/fcl"

export const TPSAccountResolver = ({ accounts }) => (account) => {

// Select an available account address from those provided in the configuration.
const tpsPayerAddress = selectAvailableAddress(accounts)

// Select the Key ID of thekey on the Payer account that will be used during signing.
const tpsPayerKeyID = selectKeyID(tpsPayerAddress, accounts)

// Produce a unique identifier for this payer address and key id combination.
const tpsTempID = `${tpsPayerAddress}-${tpsPayerKeyID}`

return ({
...account,
tempId: tpsTempID
addr: tpsPayerAddress
keyId: tpsPayerKeyID
})
}

export const TPSSigner = ({ accounts }) => (signable) => {
// Get the address specified in the signable as the payer.
const signablePayerAddress = signable.voucher.payer

// Get the account specified in the TPSSigner configuration that corresponds
// to the signablePayerAdress.
const account = accounts.find(a => a.address === signablePayerAddress)

// Throw an error if the signablePayerAddress is not found in the TPSSigner configuration
if (!account) {
throw new Error(`TPSSigner Error: Could not find account for signablePayerAddress=${signablePayerAddress}`)
}

// For security, ensure that `signablePayerAddress` is not specified as a transaction authorizer or proposer.
if (signable.voucher.authorizers.includes(signablePayerAddress)) {
throw new Error(
`TPSSigner Error: signablePayerAddress=${signablePayerAddress} specified as a transaction authorizer in transaction signable.`
)
}

if (signable.voucher.proposalKey.address === signablePayerAddress) {
throw new Error(
`TPSSigner Error: signablePayerAddress=${signablePayerAddress} specified as the transaction proposer in transaction signable.`
)
}

// Encode the signable using WalletUtils
const encodedMessage = WalletUtils.encodeMessageFromSignable(signable, signablePayerAddress)

// Prodce a signature, as a hex string
const signature = toHexString(YourFavouriteCryptoUtility.sign(encodedMessage, account.privateKey))

return signature
}
```

The user of the TPS utility would be responsible for maintaining that the accounts they specify into `TPSAccountResolver` and `TPSSigner` as configuration have a sufficient FLOW balance at all times to pay for transactions they sign for.

> To re-iterate, this is just an example implementation of what _might_ work. Your
implementation may vary extensively.

### Drawbacks

Since the TPS would be responsible for paying for transactions, it should have suffient
security mechanisms in place to safeguard itself.

Since tranaction fees can be dynamic, increasing depending on the amount of work the network
would need to perform for each transaction, the payer service should be smart enough to take
this into consideration as needed.

### Dependencies

The Transaction Fee Payer Service would depend on FCL. Should there be changes to any aspect
of FCL that may impact the TPS, the service would have to be updated accordingly.

The Flow team communicates out all breaking changes to the community consistently and with sufficient time to make any updates.

### Engineering Impact

This is a rather large project. While an MVP could be built by an engineering team, a full solution
would likely require multiple participants and skillsets.

### Compatibility

This project would need to maintain compatibility with latest versions of FCL.

## Questions and Discussion Topics

- Are the other approches to desiging a TPS that differ from the example provided in this FLIP?