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

Fuel Improvement Proposal: Retryable Input Messages #439

Closed
pixelcircuits opened this issue Dec 4, 2022 · 16 comments · Fixed by #443
Closed

Fuel Improvement Proposal: Retryable Input Messages #439

pixelcircuits opened this issue Dec 4, 2022 · 16 comments · Fixed by #443
Assignees
Labels
enhancement New feature or request

Comments

@pixelcircuits
Copy link
Contributor

Fuel Improvement Proposal: Retryable Input Messages

The core of this proposal is to make InputMessages retryable at the protocol level. This means that InputMeassages included in a transaction that ends in a revert are considered unspent and can be included in future transactions.

Why is this Useful?

There are multiple reasons why this would be desirable, but the biggest would be to reduce the likelihood of a dropped message to essentially zero. A dropped message is one that intends to deliver a payload of data or be used in a specific way, but is considered spent before it can. One example of this would be an ERC20 bridge transaction where coins are locked up on Ethereum, but the corresponding mint on Fuel never happens. This would be catastrophic for any multi chain async processes. We currently try to safeguard from dropped messages by using a predicate that checks for a correctly constructed transaction complete with a set amount of gas to prevent out-of-gas reverts. The issue with this approach is that the predicate root is hardcoded in the Ethereum side solidity contracts, meaning we can't reasonable change it once contracts get published. We also have the issue of picking arbitrary gas amounts for a predicate to consider as "reasonable" enough to at least register a message payload into some contracts state to execute later. The hardcoded gas requirements of the predicate could get us in trouble if any gas repricing occurs on the Fuel client. There's also the concern of developers not keeping track of how much gas their messages are using in conjunction with the predicate we popularize. We could save ourselves and other developers potential future headaches if messages we just retryable in the case of a revert.

Implications

In order to make a message retryable, it would have to be impossible for a messages value (amount) to be spendable on gas.
This is necessary since a transaction that reverts would have it's message inputs be retryable and, since a transaction that reverts still needs to pay for gas, the gas needs to come from an input other than the message input itself. This will add more complexity in the fuel client as it will have to keep track of two separate buckets of free balance (one that can pay for gas and one that is simple transferable). This also adds a minor complication to the process of bridging ETH from Ethereum to Fuel. When bridging ETH to Fuel, there will now be an extra step required to make the ETH spendable on gas (the most common reason for bridging). This can be annoying to a first time user trying to fund a wallet for gas (there would no longer be a way to fund a wallet for gas without another already funded wallet helping out). A lot of this can be alleviated via liquidity based bridges like Hop/Connext or our own message executor service converting messages to simple coins. This does also complicate how the first account on Fuel gets funded, the easiest solution being that the first few genesis blocks be permissioned with a gas fee of 0 before opening the chain up to anyone with our message executor service. Another solution would be to mint tokens on the Fuel chain at genesis which we swear to only use for gas and then burn the excess after normal ETH bridging brings over enough ETH to sustain the chain.

Other Benefits

Other benefits besides removing the arbitrary gas tracking requirements include:

  • The getSpendableResources graphql endpoint would no longer have to return messages along with coins (messages can just be their own thing to query for and most developers won't have to think about them).
  • Checking an account balance (balance) no longer needs to include messages (again, messages can just be considered their own thing).
  • The predicate we use for bridging gets reduced in complexity and byte size (only has to worry about checking the transaction script).
  • Developers don't have worry about complexity and gas usage in their message handling (big improvement for complex cross chain interactions).
@pixelcircuits pixelcircuits added the enhancement New feature or request label Dec 4, 2022
@pixelcircuits pixelcircuits self-assigned this Dec 4, 2022
@freesig
Copy link
Contributor

freesig commented Dec 5, 2022

I'm not sure I understand how retrying a transaction that previously reverted because it did not have enough gas would ever pass again in the future?

@Braqzen
Copy link
Contributor

Braqzen commented Dec 5, 2022

I don't have any concrete suggestions but I have some concerns

  1. Could a retryable message (upon revert) be used as a denial of service vector?
    • I don't see how it would differ from a regular Tx being submitted by a user after it reverted
  2. Trust
    • A permissioned system won't look good even if executed honestly because at that point you have established a track record of control. If such a system is never mentioned then it won't be a consideration for a user
    • Minting tokens and saying "we promise" is also against the idea of crypto. There's also the implication that this mint will have a direct impact on the asset itself
  3. The UX for funding will need to be carefully considered by the tooling side / UI side. As long as that is done well then it shouldn't be that much of an issue to learn something new

@pixelcircuits
Copy link
Contributor Author

I'm not sure I understand how retrying a transaction that previously reverted because it did not have enough gas would ever pass again in the future?

It's not that the transaction would be retryable, just that the message wouldn't be considered spent and could be included in another future transaction. The most common scenario would be that there was not enough gas to finish processing the message, but other possible reasons for a revert could also happen, like the message wanting to be relayed to a contract that wasn't included as part of the transaction inputs.

@pixelcircuits
Copy link
Contributor Author

  1. Could a retryable message (upon revert) be used as a denial of service vector?

This is why the proposal includes the stipulation that a messages value can't be spent on gas. All other inputs on the reverted transaction will be considered spent except for the InputMessage. (gas will still be consumed even during a revert)

@pixelcircuits
Copy link
Contributor Author

3. The UX for funding will need to be carefully considered by the tooling side / UI side. As long as that is done well then it shouldn't be that much of an issue to learn something new

Developers (and more impactfully, users) currently already have to learn something new with how messages are just as spendable as coins. This would eliminate the need for users to understand the difference and developers who aren't dealing with messages no longer have to worry about what a message is when trying to find coins to pay for gas.

@pixelcircuits
Copy link
Contributor Author

  • Minting tokens and saying "we promise" is also against the idea of crypto. There's also the implication that this mint will have a direct impact on the asset itself

I agree that the setup will be the trickiest part. The suggestion of doing an initial mint that only pays for gas on the first couple of ETH bridgings and then all excess from that mint gets burned (and no more minting can occur) can be verified on chain. After this whole setup process is done, there is no more need for trust. I see this as similar to when an ownable contract is deployed with an EOA as the owner, that then transfers ownership to a multisig. As long as the end result of the setup process is something I like, I don't care what happened temporarily during it.

@xgreenx
Copy link
Contributor

xgreenx commented Dec 5, 2022

Did I get the problem right?

Because amount is part of Message, we can use it as input for the base token. But we can have cases where it is zero, or we want to use only the base token without consumption of data, or use data without consumption of the base token.

If yes, it may be simpler to split this functionality into two. One for bridging the data field(let's call it MessageData during this conversation) and one for bridging the amount(let's call it MessageCoin during this conversation). MessageCoin acts as a standard Coin but without utxo_id, tx_pointer, and asset_id. MessageData is a new type of input(or we can introduce a new entity for this kind of data like Consumable) that will be consumed only on successful execution of the transaction.

@pixelcircuits
Copy link
Contributor Author

Did I get the problem right?

The core problem is that it's hard to guarantee that a message will be included in a transaction where everything goes according to plan (no out-of-gas issues, incorrect or missing inputs, general logic failures). Any sort of failure and the message is essentially gone forever (dropped) as it will now be considered spent. We've safeguarded against this as best we can by using a predicate to ensure a message is included in a successful transaction, but there could still be edge cases that pop up. The biggest threat being gas repricing that screws up developers previously calculated gas requirements which they may have hardcoded into contracts and we're even hard coding into the predicate we've developed for the bridge.

Splitting up the message data and amount, limits some of the more complex multichain interactions which I know both Arbitrum and Optimism currently support (for example, cross chain meta transactions).

Another approach might be to create a contract on Fuel at genesis that handles all things related to the base asset. I believe this is already desired in order to do things like flash loan the base asset. Then the client can basically treat the amount field as just another blob of data and base asset handling would only occur via the Fuel base asset contract recognizing the message and minting/burning according to whatever rules. The biggest downside to this would be a hit on parallelization if this base asset contract starts getting overly popular. We'd also have to redesign how output messages work (all output messages would have to be made through the base asset contract).

@Voxelot
Copy link
Member

Voxelot commented Dec 5, 2022

Maybe instead of splitting message inputs into two types, we add a new message output type that captures the input data in the case of a revert? While the amount on the new message output would be reduced by the amount of gas spent, we'd still get to preserve the data and they could bridge more funds over to cover gas as needed without losing special metadata tied to a unique asset like an NFT.

@pixelcircuits
Copy link
Contributor Author

Maybe instead of splitting message inputs into two types, we add a new message output type that captures the input data in the case of a revert? While the amount on the new message output would be reduced by the amount of gas spent, we'd still get to preserve the data and they could bridge more funds over to cover gas as needed without losing special metadata tied to a unique asset like an NFT.

With the current predicate based approach we're using (where we set the recipient as a predicate so anyone can "spend" the message as long as transaction requirements are met, like including the intended target contract) an attacker could drain the amount on a message by repeatedly submitting reverting transactions with the message at no cost to them. Also, remember that the amount on a message could be intended for anything and not just paying for gas. Like if an Ethereum contract wanted to send ETH to a Fuel contract, it would require a message with both an amount and data.

@Voxelot
Copy link
Member

Voxelot commented Dec 6, 2022

Couldn't the message predicate validate the script of the tx to prevent abuse?

@pixelcircuits
Copy link
Contributor Author

Couldn't the message predicate validate the script of the tx to prevent abuse?

I was thinking that the predicate no longer checks for gas requirements since part of this proposal was to eliminate the need for the predicate to hardcode arbitrary gas amount checks. If it doesn't check for gas then it would be easy to craft a transaction that results in a revert due to out-of-gas.

@Voxelot
Copy link
Member

Voxelot commented Dec 6, 2022

What if messages were literally just serialized script txs (maybe with some extra constraints) and the block producer automatically included them into the block (similar to a Coinbase mint tx) only if it's able to be processed?

This way we don't have the complexity of users or a service having to drive messages, and potentially lose sensitive message data. If the message gets stale / isn't includeable, we could have a timeout that makes it withdrawable by a root of expired messages on the block header, allowing the assets to be recovered. This could be fraud-provable by allowing others to submit a proof that the message was already used on chain (via the txs root).

@pixelcircuits
Copy link
Contributor Author

What if messages were literally just serialized script txs (maybe with some extra constraints) and the block producer automatically included them into the block (similar to a Coinbase mint tx) only if it's able to be processed?

Who pays for the gas? This would also require Ethereum contracts to have to emit relatively large blobs of data for the transaction script bytecode. Part of me does envision the "message executor" duties eventually being baked into the responsibility of the block producer as part of producing valid blocks, though.

We could make the block producer be the only one allowed to spend message inputs and force that they have to be tried as part of the consensus rules. And then remove the whole predicate stuff we've been doing and just have the block producer essentially register the message to an "IncomingMessages" contract state, which can then be used by anyone to "execute" their messages. This way they will still be registered to the contract in an unspent state if the transaction reverts. This might cause a bottle neck in parallelization though since it introduces what is likely to be a very popular contract.

@pixelcircuits pixelcircuits linked a pull request Dec 9, 2022 that will close this issue
@Voxelot
Copy link
Member

Voxelot commented Dec 9, 2022

Who pays for the gas? This would also require Ethereum contracts to have to emit relatively large blobs of data for the transaction script bytecode. Part of me does envision the "message executor" duties eventually being baked into the responsibility of the block producer as part of producing valid blocks, though.

Wouldn't the message still include a fee amount that's claimable by the block producer?

just have the block producer essentially register the message to an "IncomingMessages" contract state, which can then be used by anyone to "execute" their messages.

  1. This still wouldn't solve the problem of funding users on fuel with the gas required to make a potentially fallible transaction.
  2. Interacting with contracts should remain an application-level concern to the best extent possible. If we are trying to bake integrations between contracts and consensus rules, we should consider how that contract logic could be built-in to the native protocol logic instead. I'm assuming this is what you already meant, but just wanted to clarify.

To me, a contract of unspent messages leaves us in a similar state to what we currently have with the unspent message input set. The main difference being it preserves message data and possibly causes a new kind of bottleneck.

Going back to the idea of reverted message data being captured into something similar to a "change" output, I want to dig into to your previous point about predicates being drainable:

attacker could drain the amount on a message by repeatedly submitting reverting transactions with the message at no cost to them

Since the reverted message output should have a new messageID (based on the output index etc), wouldn't that prevent replay attacks?

The benefits of capturing failed message data into a new output are:

  1. We don't need to do any special faucet or consensus rules to onboard new users with coins to pay for gas (which is a potential sybil/DOS vector)
  2. It follows the UTXO model more closely, namely that inputs are always destroyed, but values can be captured into new outputs.
  3. If a user runs out of gas, they can simply bridge some additional gas coins over to make further retry attempts.

Alternately, if we go with your approach of retryable messages I think we should consider bringing back some kind of protocol level deposit/withdrawal events which are only for gas funding. I'm not a fan of adding the extra complexity of deposit / withdrawal events to the block headers and L1 in addition to messages, but it seems more robust than relying on a faucet or simply giving "new" users free txs.

@Voxelot
Copy link
Member

Voxelot commented Dec 9, 2022

that checks for a correctly constructed transaction complete with a set amount of gas to prevent out-of-gas reverts

Couldn't we work around this by making the predicate less strict and not hard-coding as much about the script tx?

There's also the concern of developers not keeping track of how much gas their messages are using in conjunction with the predicate we popularize.

Could you elaborate on what you mean by this? Is the issue that other L1 apps bridging to Fuel may not include a sufficient amount of gas on the messages to cover any changes in our predicate? It seems like this would still be concern in the future if we upgraded any of the standard contracts like the ERC20 gateway. Typically users decide how much gas they want to include while bridging. For example, if they already have a good amount of gas on fuel they might leave the message amount as zero because they already have enough gas bridged over to spend the new message.

If they run out of gas, they can always bridge over some vanilla messages with a gas amount to top up their account.

The issue with this approach is that the predicate root is hardcoded in the Ethereum side solidity contracts,

Couldn't this be solved with a contract update/upgrade feature on ethereum? Since it's already fairly common in other bridging designs to bake the tx call logic into the bridge contract (i.e. xcmp or other l2's etc) that shouldn't be a dealbreaker.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants