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

Support for offchain keyword #12610

Closed
beerefine opened this issue Feb 1, 2022 · 5 comments
Closed

Support for offchain keyword #12610

beerefine opened this issue Feb 1, 2022 · 5 comments
Labels
closed due inactivity The issue/PR was automatically closed due to inactivity. language design :rage4: Any changes to the language, e.g. new features stale The issue/PR was marked as stale because it has been open for too long.

Comments

@beerefine
Copy link

beerefine commented Feb 1, 2022

Abstract

A lot of expensive sanity checks can be performed off-chain. I propose a new solidity keyword with simple, familiar syntax and fully compatible with current EVM opcodes.

Motivation

Gas costs increase significantly with complexity. Moreover, a lot of information present on-chain can be fetched off-chain, saving significant gas for storage access.

Specification

Similarly to the unchecked keyword, I propose a syntax for offchain blocks:

offchain {
    // off-chain logic here, e.g.
    require(token.balanceOf(msg.sender) >= amount, "insufficient balance");
}

To avoid contradictions between off-chain & on-chain logic, the block must behave like view-only or pure functions, such that it cannot affect the flow of execution aside from reverting.

Which is equivalent to:
if (isOffChain) { ... }

isOffChain can be repurpose block.coinbase, since it is unknown during offchain execution anyway. So now the above code can be translated to:

if (block.coinbase == OFFCHAIN_MAGIC) { ... }

Note COINBASE opcode costs only 2 gas. I suggest OFFCHAIN_MAGIC to be uint256(0). this way the new keyword can be implemented with just 4 opcodes overhead:

COINBASE             // 2 gas
ISZERO               // 3 gas
JUMPI endOfBlock     // 10 gas
    <offchain logic here>
endOfBlock:
JUMPDEST             // 1 gas

The extra cost will be 2 + 3 + 10 + 1 = 16 gas, while potentially saving significantly larger amounts. Moreover, the cost of the off-chain block can be calculated and if it is smaller than 16, the offchain keyword can be optimized out.

Caveats

Users of the offchain keyword need to be careful not to introduce security holes. Here is a safe and an unsafe example:

safe:

offchain {
    require(token.balanceOf(msg.sender) >= amount, "insufficient balance");
}
token.safeTransferFrom(msg.sender, address(this), amount); // if user doesn't have enough balance, this will fail anyway

unsafe:

offchain {
    require(recipient == ecrecover(hash, v, r, s), "invalid signature");
}
token.safeTransferFrom(address(this), recipient, amount); // the previous validity check must be performed on-chain

Backwards Compatibility

Since no new EVM opcodes were introduced, it is compatible with existing EVM chains.
Nothing breaks on the compiler side, and additionally the offchain keyword can be ignored with a warning for older solidity versions, with the side effect of producing more expensive bytecode.

@cameel
Copy link
Member

cameel commented Feb 1, 2022

That's an interesting idea. Very similar to how for example in C/C++ you have the assert() macro that only generates code in debug builds to avoid any runtime penalty in production ones. has similar downsides though.

Unfortunately I don't think it would work out in practice. This would need support from blockchain clients and the flow would probably be something like this:

  1. You submit your signed transaction to the node.
  2. Node executes it off chain first to make sure all your offchain checks pass. If they don't you get an error.
  3. Node actually propagates your transaction.

The problem is that the fact that your checks passed in the initial execution does not mean they would pass on-chain too. Unless all your offchain logic is pure, the result might be different depending on what other transactions run before it - the node can't know that until your transaction actually published as a part of a block.

Also, some general remarks:

  • I'm not sure if there's a good use case for it being a general mechanism that can handle arbitrary statements. I think that doing it for require() and assert() would cover most practical situations. This would allow a more concise syntax, e.g. offchain require( ...);.
  • Ensuring that the content of the block is view/pure is not enough IMO. It should also for example disallow assignments to local or memory variables. I'd also disallow assembly. One of the pitfalls of the assert() macro is that you can do things that have side-effects and your code might fail in production where those side-effects are no longer present.
  • If this code is never executed on-chain you unnecessarily pay for it to be posted there. It might make more sense to pass it to the clients out of band, just like this is the case with the ABI.

@cameel cameel added feature language design :rage4: Any changes to the language, e.g. new features labels Feb 1, 2022
@cameel cameel added this to New issues in Solidity via automation Feb 1, 2022
@beerefine
Copy link
Author

beerefine commented Feb 7, 2022

you understand the flow perfectly. and yes, as I mentioned the offchain block should be pure. I agree that what pure means in the context of a code-block rather than an entire function is different and it should not allow side effects such as writing to memory outside of the block. so thank you for that good observation. however, I do not agree about your arguments:

  1. "This would need support from blockchain clients" - no, because the offchain keyword is only syntactic-sugar for things you can accomplish with extra few characters of code (if (block.coinbase == address(0)) { .. }) and so is transparent to clients.
  2. "the result might be different depending on what other transactions run before it" - that is true regardless, any transaction you emulate offchain by call() might disagree with what you actually send() on chain (frontrunning, gas mismatch, conditions based on timestamp, block number, etc.) so it is no argument against the proposed keyword.

and lastly regarding your last remark: the whole point is gas saving, the one-time payment of few extra weis for the extra code is completely negligible compared to the overall gas savings. and what you suggest with extending the ABI would actually require clients support.

considering the code-block will indeed have no side effects, I can't see any other drawbacks for this syntax. although I am open to be enlightened and learn otherwise :)

edit: I have to admit I did not check a single client's given block.coinbase. so long the majority do have predictable coinbase I believe it's fine. otherwise, perhaps some other logic may be used instead.

@cameel
Copy link
Member

cameel commented Feb 7, 2022

keyword is only syntactic-sugar (...) and so is transparent to clients.

What I meant is that you need support from clients to execute it. Otherwise it's just a piece of dead code that's useless without client-side support.

any transaction you emulate offchain by call() might disagree with what you actually send() on chain (frontrunning, gas mismatch, conditions based on timestamp, block number, etc.) so it is no argument against the proposed keyword.

I mean, this was more of an argument against executing it off-chain at all. A require() executed off-chain is just not reliable enough in that sense compared to one that gets executed on-chain.

@github-actions
Copy link

This issue has been marked as stale due to inactivity for the last 90 days.
It will be automatically closed in 7 days.

@github-actions github-actions bot added the stale The issue/PR was marked as stale because it has been open for too long. label Mar 22, 2023
@github-actions
Copy link

Hi everyone! This issue has been automatically closed due to inactivity.
If you think this issue is still relevant in the latest Solidity version and you have something to contribute, feel free to reopen.
However, unless the issue is a concrete proposal that can be implemented, we recommend starting a language discussion on the forum instead.

@github-actions github-actions bot added the closed due inactivity The issue/PR was automatically closed due to inactivity. label Mar 29, 2023
@github-actions github-actions bot closed this as not planned Won't fix, can't repro, duplicate, stale Mar 29, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
closed due inactivity The issue/PR was automatically closed due to inactivity. language design :rage4: Any changes to the language, e.g. new features stale The issue/PR was marked as stale because it has been open for too long.
Projects
None yet
Development

No branches or pull requests

3 participants