sui icon indicating copy to clipboard operation
sui copied to clipboard

Support sponsored transactions

Open lxfind opened this issue 3 years ago • 24 comments

Feature Request: Sponsored transactions

Support a transaction type where TxContext::sender() is address A, but the the gas object that pays for the transaction belongs to address B. Such a tx will be first signed by A, and then signed again by B.

This feature is sometimes called "meta-transactions" or gasless transactions on other platforms. We use the term "sponsored" because our sponsored transactions are ordinary (not meta), and someone still pays for the gas, so "gasless" doesn't quite fit.

Motivation

The requirement to acquire tokens before sending transactions is a huge pain point. Users need to sign up at exchange, KYC, connect debit card, go through a waiting period before using tokens, … In addition, entities that want to manage keys for their users must become custodians (a very stringent process) because every account that wants to take action needs tokens.

Sponsored transactions alleviate this by decoupling sending transactions and holding tokens. This also helps in non-omnibus custodial settings (e.g., a custodial game server w/ separate Sui accounts for each user). Without sponsored transactions, the game server would have to carefully (and frequently) send txes to top up the gas coins in each player account so it can subsequently sign/send transactions on behalf of the player. But with them, it can hold all the gas in an admin account (or a few admin accounts).

Comparison to other platforms

This feature exists on other platforms, but we can implement this feature generically, which will be significantly simpler for end-users and programmers, and also more cost-effective.

  • In Eth, meta-transactions aren't part of the protocol. A contract has to explicitly "opt-in" to supporting meta-transactions by implementing EIP2771. If it doesn't, you have use normal transactions.
  • A user that wants to send a meta-transaction has to choose a forwarder contract implementation to use. This contract accepts transactions sends by the gas payer, then interprets the calldata as a transaction sent by the user (reflection!), hence the name "meta-transaction" (whereas our implementation is more direct--no reflection or meta-step).
  • This is a lot more expensive than sending an ordinary transaction, and potentially scary because you have to trust the forwarder, which is a third-party contract.
  • In Sui, there's no need for a programmer to "opt in" to supporting gasless transactions--it won't even be possible to tell the difference between a gasless tx and a normal tx from inside Move.
  • In Sui, there's no need for a forwarder.
  • In Sui, gasless txes should probably cost a bit more than ordinary ones (will include one extra signature, address, and pubkey, and do one extra signature check), but hopefully not much. And hopefully gas fees are so low that the difference won't be very noticeable
  • The Sui data model makes it much simpler to implement a "gas payer" service. In Eth, the sequence number system forces you to send 1 tx at a time from an account, so a gas payer service would have to manage many accounts/keys (or service only one user at a time). In Sui, a single account with multiple gas objects allows the payer to support arbitrarily parallelism while managing only one key.

Suggested implementation

  • Add new field to tx that stores an optional gas_signer and gas_signature
  • Handle that field properly in the tx execution path
  • Storage rebates from executing the transaction are paid to the gas_signer

lxfind avatar Jun 04 '22 17:06 lxfind

One thought on an interesting design question for this: who gets the storage rebates in such a tx? You can implement gasless transactions to return the storage rebate to either (a) the sender, or (b) to the gas payer, or (c) include a flag in the tx that lets you choose.

Option (a) is intriguing because it gives you a way to get SUI tokens into an account that didn't previously have them without going through an exchange. Option (b) is intriguing for the business model/incentives of "gas stations" (i.e., gas payer entities): if you're an end-user with no SUI who wants to send a tx that costs X, but frees Y > X worth of storage that goes to the gas payer, a gas station should be willing to pay X and pocket Y - X profit without charging the user anything.

sblackshear avatar Jun 04 '22 19:06 sblackshear

(a) seems a strange way to give people SUI tokens. If the payer wants to give the tx sender SUI tokens, they can just send them? This implicit behavior (basically making a meta-tx a combination of gasless tx + gas faucet) seems counter-intuitive and could probably be easily abused.

lxfind avatar Jun 05 '22 02:06 lxfind

Some discussions: https://mysten-labs.slack.com/archives/C02D44HQ2FP/p1654370023166769?thread_ts=1654292138.057289&cid=C02D44HQ2FP

lxfind avatar Jun 05 '22 02:06 lxfind

@lxfind: I fleshed out the original issue description to include some of the external discussions, and a simple implementation proposal--hope that's ok!

sblackshear avatar Jun 09 '22 23:06 sblackshear

I wanted to clarify the proposed implementation, specifically:

  1. the sponsored transaction does not have to be known ahead of time by the gas_signer
  2. the gas_signer is only granting usage of a single gas-object, and not any other objects they own

On Solana, you can pay for someone else's transaction by making yourself the fee_payer, and then signing the transaction (Solana supports signatures from multiple accounts). However, you must know the transaction ahead of time to sign it (obviously). This limits the use-case of sponsored transactions.

An example use case might be an on-chain poll. Suppose I want users to be able to submit their response to a poll, directly on-chain, but I don't know in advance what their response will be (obviously), and I want to pay the gas-fee on their behalf. In that case, I send the user a gas_signer and gas_signature, and they fill in the arguments and submit the transaction.

I suppose the risk of paying for unknown transactions is that the user may choose to do a very costly transaction, or they may lock my gas-object up for an epoch by submitting conflicting transactions. The first issue could be mitigated by specifying a gas-budget along with the gas_signature. I'm assuming gas-signatures are only good for one transaction, so if the tx fails the first time, they must obtain a second gas-signature from me? The second issue is a little more complex, but if I have many gas-objects to assign to users it shouldn't be a big issue.

Overall I think this will be a powerful primitive.

PaulFidika avatar Oct 08 '22 23:10 PaulFidika

We've also been thinking about designs for sponsored transactions at Shinami. The approach we are leaning towards is to keep the interface for sui_executeTransaction the same, but instead supplement it with a new method sui_metaWrapTransaction that accepts the encoded payload of an existing txn, as well as an additional gas object and budget that are used for execution on the sui node.

We like this approach because there is no new friction introduced to the end user e.g. doesn't need to explicitly encode a sponsor's gas object, and it gives an open opportunity for third parties to enter and offer sponsorship services.

@PaulFidika to go with your poll example, users who want to participate in the poll would bundle their vote into a sui_executeTransaction transaction builder as normal, and submit it to either you (if you decide to take on the sponsor role) or to another third party sponsor entity. The sponsor would take the sui_executeTransaction payload, pass it into sui_metaWrapTransaction as an argument along with the sponsor's own gas object and specified budget, the sponsor's signature and public key, and submit it to a sui node for execution. The sui node would unwrap the payloads, perform validation, and execute with the sponsor's gas object. One question wrt to your poll example; are concerned about the context of a sponsor intercepting the original transaction payload?

Any feedback on this approach is welcome!

jachen-sh avatar Oct 09 '22 02:10 jachen-sh

I think I only half understand your implementation suggestion, but I think that if the sponsor has to sit in the middle and receive requests, sign them, and submit them on behalf of the end user, that creates a lot of burden for the sponsor in terms of maintaining liveness. I was thinking of it as more like 'hey here is my gas object signature and do whatever you want with it'; that signature could be generated by the sponsor's webserver on page-load and then forgotten about.

But yeah, as you pointed out, another flaw in your wrapper idea is that the sponsor could censor the signer's transactions (in this case, if they don't like the poll response), which kind of defeats one of the main advantages of blockchain in general. But then again the user is depending upon and using a third party's gas object, so they can't complain too much lol

PaulFidika avatar Oct 09 '22 04:10 PaulFidika

These two approaches may not conflict though. We could redefine the SenderSignedData (https://github.com/MystenLabs/sui/blob/44436ed30a6d7533c42602b4a808505eeec99c21/crates/sui-types/src/messages.rs#L644) as the following:

pub struct SenderSignedData {
    pub tx_data: TransactionData,
    /// tx_signature is signed by the transaction sender, applied on `data`.
    pub tx_signature: Signature,
    pub gas_data_and_sig: Option<(GasData, Signature)>,
}

pub struct TransactionData {
    pub kind: TransactionKind,
    sender: SuiAddress,
}

pub struct GasData {
    gas_payment: ObjectRef,
    pub gas_price: u64,
    pub gas_budget: u64,
}

First, a user could use the transaction building API to build a transaction without gas data, sign the tx_data (which does not commit to any gas data) only, send it to a gas station. The gas station can then provide a gas payment, sign it and fill the field gas_data_and_sig. The transaction is then complete and can be sent over. Alternatively, a gas station could also just provide a valid gas_data_and_sig pair, send it to the user, and user can put it in their transaction. Finally, a user can also just use their own gas data and signature.

credit: @patrickkuo on coming up with this idea.

lxfind avatar Oct 09 '22 05:10 lxfind

Okay I see what you're saying; make the data-signature and the gas-signature of every transaction completely separable, so they can be done in either order by different entities. Yeah that seems very flexible to me.

PaulFidika avatar Oct 09 '22 06:10 PaulFidika

Having a gas_data_and_sig pair that can be easily moved around and attached to a transaction at various stages sounds very interesting. I wonder if there's some way to provide some basic constraints on which transactions it can attach to. I'm trying to think through if it might be possible to "steal" a gas_data_and_sig pair and use it on an inappropriate transaction. If it had optional transaction attributes itself then you could potentially enforce that it's only used on the transactions it was intended for...

jaredcosulich avatar Oct 09 '22 10:10 jaredcosulich

Having a gas_data_and_sig pair that can be easily moved around and attached to a transaction at various stages sounds very interesting. I wonder if there's some way to provide some basic constraints on which transactions it can attach to. I'm trying to think through if it might be possible to "steal" a gas_data_and_sig pair and use it on an inappropriate transaction. If it had optional transaction attributes itself then you could potentially enforce that it's only used on the transactions it was intended for...

The gas signature should be signed over TransactionData + GasData (or "Sender's TX signature" + GasData) to prevent "stealing"

patrickkuo avatar Oct 09 '22 10:10 patrickkuo

In the situation where "a gas station could also just provide a valid gas_data_and_sig pair, send it to the user, and user can put it in their transaction" - couldn't that valid gas_data_and_sig pair be used inappropriately? The gas station wouldn't know the exact TransactionData beforehand necessarily. Or maybe I'm misunderstanding that suggested context.

jaredcosulich avatar Oct 09 '22 11:10 jaredcosulich

imo I think it will be safer if the gas station handle the execution as well; however if we are supporting "a gas station could also just provide a valid gas_data_and_sig pair, send it to the user, and user can put it in their transaction", the gas station will need to know the TransactionData to get the rough cost of the TX so they can provide a appropriate gas object with enough gas for that transaction, the gas station will want to avoid providing too much gas because there are no control on when the sender will execute the transaction in this situation.

patrickkuo avatar Oct 09 '22 11:10 patrickkuo

It's interesting to see the two designs that emerged here:

  • Online sponsorship, where the sponsor / gas station is involved in the loop for TX signing and execution. The sponsor can see and sign over TransactionData, so it gives them some control over what they're sponsoring. The flip side is the sponsor could potentially censor the TXs. Also it requires the sponsor to operate this online infrastructure. However, if the sender chooses to use a node service to submit their TXs (and implicitly trusts the neutrality of it), the sponsorship can be built into the node service to provide the most frictionless user experience to the sender.
  • Offline sponsorship, where the sponsor hands out signed gas objects that are TX-agnostic. Anyone in possession of the signed gas object can submit a TX with it. This is like handing out checks with a blank payee field, but their usage is restricted to only gas. However, the sponsor needs to be very careful in distributing the signed gas objects, most likely in an authenticated one-to-one setting. Also, it's worth noting that the entire gas object needs to be "locked" from the sponsor side after it's handed out, because any usage of it would invalidate the distributed gas object. This in practice would require the sponsor to maintain a large pool of small gas objects, and implement some kind of expiration / recycling mechanism on the signed gas objects to deal with the case where the recipient doesn't spend it soon enough. This mode makes it flexible to be able to sponsor a few one-off TXs, but operating it at scale would still require non-trivial infra from the sponsor side.

I can see merits in both approaches.

jasonxh avatar Oct 09 '22 17:10 jasonxh

+1 to the proposed gas_data_and_sig changes. This gives flexibility to sponsors on how to offer their services, either through a more censorship resistant 1:1 authenticated setting, or through a more frictionless txn forwarding setting. I do think wrt censorship in the latter approach, having multiple competitive sponsors in the ecosystem would prevent this from happening.

jachen-sh avatar Oct 09 '22 18:10 jachen-sh

It's interesting to see the two designs that emerged here:

  • Online sponsorship, where the sponsor / gas station is involved in the loop for TX signing and execution. The sponsor can see and sign over TransactionData, so it gives them some control over what they're sponsoring. The flip side is the sponsor could potentially censor the TXs. Also it requires the sponsor to operate this online infrastructure. However, if the sender chooses to use a node service to submit their TXs (and implicitly trusts the neutrality of it), the sponsorship can be built into the node service to provide the most frictionless user experience to the sender.
  • Offline sponsorship, where the sponsor hands out signed gas objects that are TX-agnostic. Anyone in possession of the signed gas object can submit a TX with it. This is like handing out checks with a blank payee field, but their usage is restricted to only gas. However, the sponsor needs to be very careful in distributing the signed gas objects, most likely in an authenticated one-to-one setting. Also, it's worth noting that the entire gas object needs to be "locked" from the sponsor side after it's handed out, because any usage of it would invalidate the distributed gas object. This in practice would require the sponsor to maintain a large pool of small gas objects, and implement some kind of expiration / recycling mechanism on the signed gas objects to deal with the case where the recipient doesn't spend it soon enough. This mode makes it flexible to be able to sponsor a few one-off TXs, but operating it at scale would still require non-trivial infra from the sponsor side.

I can see merits in both approaches.

I like these terms; 'online sponsorship' and 'offline sponsorship', kind of like 'full proxy tx handling service' VS 'blank check fire and forget'. For the offline sponsorship; how long does it take before a transaction signature expires? A whole epoch? I imagine the offline-sponsor will hand out a lot of checks, and then after every epoch garbage-collect (merge) the gas objects that weren't use.

Either way, I suppose both of them will require work on the part of the sponsor. Is it possible to enable both approaches? If there's a data_signature (from user) and gas_signature (from sponsor), then the two should be separable.

For the online-sponsorship, the gas_signature is provided to the user after the data_signature. For the offline-sponsorship, the gas_signature is supplied first and then the data_signature afterwards.

Something helpful might be brainstorming potential use-cases:

  • A newb-friendly wallet, that sponsors all transactions from newly signed-up users, so they can mint free NFTs without having to first buy any tokens.
  • A Web3 social-media platform, which allows users to post messages to the Sui blockchain, but doesn't want users to have to pay to post, instead using advertiser money or premium-membership money to fund its gas fees.
  • A web-based game that generates a keypair, emails it to users a backup, and uploads the user's save-data to Sui. Users can recover save-data using the private-key from the email.

If you guys have other possible use-cases feel free to drop them here.

PaulFidika avatar Oct 10 '22 00:10 PaulFidika

For the offline sponsorship; how long does it take before a transaction signature expires?

Currently there are no expiration for gas signatures.

To make it more flexible, maybe we can include a optional tx_signature in GasData, when tx_signature is specified, the gas can only be used by that transaction; if not specified, it can be used by any TX.

pub struct GasData {
    gas_payment: ObjectRef,
    pub gas_price: u64,
    pub gas_budget: u64,
    pub tx_signature: Option<Signature>,
    pub valid_until_epoch: EpochID,
}

This can allow use case where the gas station want more control on what it is paying, at the same time allow sponsors handing out minting coupons.

patrickkuo avatar Oct 10 '22 14:10 patrickkuo

I haven't thought through this enough, but it might be nice to have a middle ground between tx_signature and no tx_signature that limits the use of the GasData to transactions against a certain function, object, package, etc. All of this does increase the risk that the GasData is abused in some fashion, but I could see a middle ground between the exact transaction specified and no transaction specified being valuable.

jaredcosulich avatar Oct 10 '22 14:10 jaredcosulich

valid_until_epoch is a good idea.

We should at least make this flexible in the initial implementation so that it's easy to extend it latter. For example, we could make gas_data_and_sig an enum, start with:

  • NotPaid // implies None case
  • TxSpecificPayment(GasData, Signature) // Signature commits to both TxData and GasData

Then it would be easy to add other enum latter like:

  • FlexiblePayment(GasData, EpochID, Signature) // EpochID is valid_until_epoch, Signature commits to GasData + EpochID.

lxfind avatar Oct 10 '22 15:10 lxfind

This is kind of a different thing entirely, but it's worth noting that on NEAR, we had 'function call keys', which allow an account-owner to add a key to their account which is scoped to being able to call certain modules and certain functions, and also assigned a gas budget. This means users could assign a portion of their gas account, create a keypair, and give away the keypair to a program or person, which would allow that person to sign transactions as them, using their gas, in a limited scope.

https://docs.near.org/concepts/basics/accounts/access-keys

The scoping of gas_signatures to specific modules and functions is interesting, but I don't want to make this system more complex than it needs to be, since it's really core to Sui. So I guess it's up to you if you think the added complexity is worth the added flexibility.

And yeah, I definitely think all signatures (data or gas signatures) should have some expiration (time or epoch), so that there is always a limited time between signing and submission.

PaulFidika avatar Oct 10 '22 18:10 PaulFidika

Hi all, thanks for the discussions above, they are very inspiring especially on their flexibilities. Unfortunately, there is an inherent problem with the above scheme: since the user signature does not commit to the gas data, while it makes the transaction part and gas data totally composable, it also renders the transaction party replayable. For example, assuming the following data structure

/// this is today's TransactionData minus gas part
pub struct TransactionData {
    pub kind: TransactionKind,
    sender: SuiAddress,
}

pub struct SenderSignedData {
    pub tx_data: TransactionData,
    
    /// tx_signature binds to tx_data and is signed by user 
    pub tx_signature: Signature,

    pub gas_data: GasData, 
    /// gas_signature binds to gas_data and is signed by gas_owner 
    pub gas_signature: Signature,
}

pub struct GasData {
    pub gas_owner: SuiAddress,
    pub gas_payment: ObjectRef,
    pub gas_price: u64,
    pub gas_budget: u64,
    /// note the digest is of TransactionData, but not SenderSignedData (TransactionDigest)
    pub gas_validity: GasValidity,
}

In a properly constructed transaction SenderSignedData, tx_signature commits to tx_data. Now every party (sponsor, fullnode, validators, wiki hotspots provider etc) that has seen this SenderSignedData can steal tx_data and tx_signature, construct a new SenderSignedData with another valid pair of gas_data and gas_signature. This means the tx_data is now replayable and its objects are equivocatable. We are at the mercy of them to let go. So it is a fundamental protocol safety problem that is not acceptable.

The same problem exists as well for gas_data, except that we can enforce it to bind to a specific transaction_data.

So it turns out we need to pursue another route. Now @gdanezis presented this great design:

/// this is today's TransactionData
pub struct TransactionData {
    pub kind: TransactionKind,
    sender: SuiAddress,
    gas_payment: ObjectRef,
    pub gas_price: u64,
    pub gas_budget: u64,
}

pub struct SenderSignedData {
    pub data: TransactionData,
    /// tx_signatures is signed by the user and sponsor, applied on `data`.
    pub tx_signatures: Vec<Signature>,
}

Here, the tx_signatures is a vector of Signature, respectfully signed by user and sponsor (if user == sponsor then one sig is enough). In this design, both party sign the entire thing, with signatures committing to transaction and gas. The issue with the above design goes away. It also has the following properties:

  1. the gas can be very restritive to one transaction. Sponsor does not need to worry about gas abusing
  2. the design is in fact flexible enough to support the "empty check" use case discussed above.
  3. What's more, this scheme opens the door for native multi-party transaction on Sui

Below are 3 diagrams of how the workflow would be like:

Scenario 1, sponsors a specific transaction, user initialized. See diagram

image

Scenario 2, sponsors for a specific transaction, sponsor initialized. See diagram

image

Scenario 3, empty check. See diagram

image

The flow should be straightforward from the diagram so I don't repeat myself in words. A few notes:

  1. in the "empty check" scenario, sponsor hands out a gas object upfront. The user constructs a TransactionData it likes with the gas data, signs it, passes to sponsor to sign. Sponsor signs it and submit the fullnode. This step is invisible for the user.
  2. I personally really like the potential of "sponsors a specific transaction, sponsor initialized" scenario. In this use case, the sponsor could be an advertiser, an airdropper, or a party that wants to solicit more traffic. They can do this by gifting targeted users a well-structured, unilaterally signed TransactionData. If the user is happy about it, they sign it, without paying any gas, submit it to FN for execution. Imagine I run a game studio Lu's Better Game and I want to attract top tier players on George's Good Game. So I gather a list of users who have at least one seasonable triumph NFT, and construct a transaction which they can use the NFT as proof to claim a legendary weapon in, of course, my game, sends this billed transaction to the users, boom.

Discussion Points

  1. is adding an expiration epoch to the TransactionData a desirable thing for the sponsor and the user? Presently we do not have this feature to expire a transaction. The way it works is, the validator will refuse and sign and execute a transaction with an expired transaction. If a transaction is not included in an epoch and going to expire in the end of this epoch, validators that have executed it locally will need undo it. This is similar to the unlock/undo process we have today in epoch boundary. cc @lxfind

Looking forward to hear people's thoughts on this approach.

longbowlu avatar Nov 02 '22 00:11 longbowlu

Thanks for the detailed writeup @longbowlu ! TX replay-ability is a great point. The fact that the prevention of it relies on signing over the gas object was indeed omitted in previous discussions. (On a side note, I do wonder how the current mechanism is implemented, where a user can submit a TX w/o specifying a gas object, and one would be chosen for them? Is that chosen on the client side or validator side?)

I like the proposal above, as it looks really close to a general multi-sig TX scheme, which should be able to unlock even more use cases. However, IMHO, all three flows illustrated above fall in the category of online-sponsorship, as in the sponsor is always involved in a critical step in TX construction and submission. That being said, the flexibility in who initiates the flow is very powerful. In particular, I think scenario 2 above can actually be used as a solution to the "unbiased poll" problem described by by @PaulFidika (https://github.com/MystenLabs/sui/issues/2418#issuecomment-1272455263), by the sponsor pre-constructing all poll responses and providing sponsorship to all of them. Then respondents won't have to second guess the sponsor for dropping their responses due to bias.

jasonxh avatar Nov 02 '22 01:11 jasonxh

(On a side note, I do wonder how the current mechanism is implemented, where a user can submit a TX w/o specifying a gas object, and one would be chosen for them? Is that chosen on the client side or validator side?)

We actually do not support submitting a transaction without specifying gas object. I think you were referring to transaction building api along with gas selection. Today fullnode has a transaction building api where gas_object could be optional in some endpoints. The way it works is that, fullnode chooses one gas for the user, constructs the transaction and passes it back to the user. Then user calls executeTransaction to submit the signed tx.

However, IMHO, all three flows illustrated above fall in the category of online-sponsorship, as in the sponsor is always involved in a critical step in TX construction and submission.

you are right, because it requires the transaction to be dual signed after the full construction, both sides need to be involved before submission.

I think scenario 2 above can actually be used as a solution to the "unbiased poll" problem described by by @PaulFidika (https://github.com/MystenLabs/sui/issues/2418#issuecomment-1272455263), by the sponsor pre-constructing all poll responses and providing sponsorship to all of them. Then respondents won't have to second guess the sponsor for dropping their responses due to bias.

precisely. I'm less worried about censorship in this case because user can easily know if the transaction happened or not. If not they could find another sponsor or simply use their own gas. But if this poll is sponsored by a biased entity, i think the entity would only provide well constructed and signed txes that itself likes :D

longbowlu avatar Nov 02 '22 02:11 longbowlu

Great detailed explanation @longbowlu ! One question I had is around the sponsor/user interactions. Those transactions can be settled off-node right? (similar to the current transaction builder methods). Would they be part of an sdk? Or maybe an api standard for the sponsor to implement?

jachen-sh avatar Nov 02 '22 04:11 jachen-sh

Closing now that this feature exists!

sblackshear avatar Apr 12 '23 21:04 sblackshear