Skip to content
This repository has been archived by the owner on Nov 15, 2023. It is now read-only.

Allow off-chain floating point operations for Wasm smart contracts #11367

Closed
kvinwang opened this issue May 6, 2022 · 18 comments · Fixed by #12469
Closed

Allow off-chain floating point operations for Wasm smart contracts #11367

kvinwang opened this issue May 6, 2022 · 18 comments · Fixed by #12469
Labels
J0-enhancement An additional feature request. Z2-medium Can be fixed by a coder with good Rust knowledge but little knowledge of the codebase.

Comments

@kvinwang
Copy link
Contributor

kvinwang commented May 6, 2022

Floating point is common used in practice, in a json parser for example. Currently, the pallets-contrat forbid floating point types in the contract codes in order to be deterministic.

As discussed in the Element group, we may add a parameter like bare_call(ALLOW_INDETERMINISM ) to allow non-deterministic operations like floats in immutable context.

@cmichi cmichi changed the title Allow floating point in ink! contracts Allow floating point operations for Wasm smart contracts May 6, 2022
@burdges
Copy link

burdges commented May 6, 2022

We cannot support real non-deterministic operations in polkadot or parachain runtimes.

@cmichi
Copy link
Contributor

cmichi commented May 6, 2022

@burdges The team that is requesting this feature is using the pallet-contracts off-chain.

@cmichi cmichi changed the title Allow floating point operations for Wasm smart contracts Allow off-chain floating point operations for Wasm smart contracts May 6, 2022
@athei
Copy link
Member

athei commented May 6, 2022

  • We allow floating points to be deployed as part of contract (guarded behind Config trait flag)
  • When hitting any floating point operation during on-chain execution we exit with an error and roll back any storage changes.
  • When executing off-chain we will allow those operations (indicated by argument to pallet_contracts::pallet::Pallet::bare_call) as we don't expect to persist any outcomes of that execution anyways
  • No benchmarks will be conducted for such instructions

@burdges
Copy link

burdges commented May 6, 2022

We do not necessarily even need restrictions then, merely that polkadot's wasm should not support floats.

If some parachain team configured their wasm time to support floats, then their chain could create invalid blocks, in that polkadot would reject them, or possibly not even compile their PVF, but if they never do so then everything remains fine. They can do this in their own fork without us altering substrate probably. In other words, polkadot would make consensus exist for this parachain.

You need restrictions only if they wish to run using their own internal finality, because floats would make their own nodes incapable of internal consensus.

@athei
Copy link
Member

athei commented May 6, 2022

If some parachain team configured their wasm time to support floats

wasmtime is not relevant here. This is about contracts.

@burdges
Copy link

burdges commented May 6, 2022

I've no idea if contracts are implemented via wasmtime or not, but my point is:

We'll always enforce determinism in polkadot, so it does not matter if a pure parachain does not internally enforce determinism since polkadot will enforce determinism for the parachain. It only matters if the parachain wants its own separate consensus.

@h4x3rotab
Copy link
Contributor

h4x3rotab commented May 7, 2022

@burdges Determinism is not always required, especially in our use case. Please allow me to add some context here.

Phala Network adopts ink! (with pallet-contracts) as its offchain computation programming model. We call it "Fat Contract". Unlike the common security model of blockchain / smart contracts, Fat Contract runs in trusted offchain workers. Its security is protected by hardware secure enclaves, rather than just consensus algorithms. Fat Contract provides FaaS-like computation. i.e. Developers can push an ink! contract to our edge offchain node, and then serve stateless requests directly, without even touching the blockchain. In this scenario, determinism is not actually required. (We do require determinism in strong consistency onchain environment though.)

Without the consensus requirement, we can do a lot of interesting things with stateless functions like heavy computation and internet access. For example, recently we are trying to build an oracle-like service by sending http request to fetch some data, and parse the json from the response. You can find a similar demo here. However the json standard includes float point. As long as we introduce any json parser, it's inevitable to trigger the float point error in pallet-contracts. Some regular expression and math libraries also introduce float points. So without this effort, the usable libraries will be very limited in ink! environment.

So here I think the solution could be, we postpone the check to runtime. The wasm code is instrumented. So we can also color the basic blocks with float point, and mark the execution as "non-deterministic" when it reaches the float point operations. In this way, we don't affect the determinism on-chain, but more use cases can be supported.

@athei
Copy link
Member

athei commented May 8, 2022

So we can also color the basic blocks with float point, and mark the execution as "non-deterministic" when it reaches the float point operations.

Is this really required? The easiest solution would be to mark a whole contract as off-chain only when it contains such instructions. Do you expect that parts (non float) of the contract will be executed on-chain?

@burdges
Copy link

burdges commented May 8, 2022

As an aside, how do we enforce no floats in polkadot? I'd always assumed we just disabled them entirely in wasmtime, so that polakdot could not even compile wasm with float instructions, which sounds safest..

@h4x3rotab
Copy link
Contributor

So we can also color the basic blocks with float point, and mark the execution as "non-deterministic" when it reaches the float point operations.

Is this really required? The easiest solution would be to mark a whole contract as off-chain only when it contains such instructions. Do you expect that parts (non float) of the contract will be executed on-chain?

Yes, Fat Contract in the first place works as a rollup solution. Although running off-chain, it still has the contract storage and reacts to the on-chain transactions where the storage can be modified. We call them mutable calls. They has the same effect as the vanilla ink!. On the other hand, it supports complex and async computation in queries (immutable calls), like what we have described above.

Immutable calls cannot live without mutable calls, because we always need some minimal strong consistent storage for config and authentication. For example, in a typical oracle use case, we may generate a signed payload with some off-chain data we got from a http request in a immutable call, and then verify it on the blockchain. In this case, we need to generate a key pair in the storage when initializing the contract, and use the private key to sign messages. If we completely disable mutable calls because of the float point introduced by a json parser, there's no chance store any key pair in the contract storage.

@athei
Copy link
Member

athei commented May 9, 2022

As an aside, how do we enforce no floats in polkadot? I'd always assumed we just disabled them entirely in wasmtime, so that polakdot could not even compile wasm with float instructions, which sounds safest..

Again, we are not talking about wastime/polkadot here. It is about contracts. They are not run in the same instance if that is what you are thinking.

@athei
Copy link
Member

athei commented Jun 22, 2022

@h4x3rotab Okay got it. It needs to be a dynamic thing then: The interpreter needs to error out when hitting such an instruction. This won't work with a JIT compiler though. At least not how the "JIT" compilers for wasm work. They are not dynamically recompiling but just compiling the whole module at once. So we wouldn't know when hitting such an instruction.

So this just got a whole lot more complicated: Adding specific requirements to wasm execution engines is something that we try to avoid in order to not be locked in with one.

@athei athei added J0-enhancement An additional feature request. Z3-substantial Can be fixed by an experienced coder with a working knowledge of the codebase. Z2-medium Can be fixed by a coder with good Rust knowledge but little knowledge of the codebase. and removed Z3-substantial Can be fixed by an experienced coder with a working knowledge of the codebase. labels Jul 28, 2022
@athei
Copy link
Member

athei commented Aug 11, 2022

Yes, Fat Contract in the first place works as a rollup solution. Although running off-chain, it still has the contract storage and reacts to the on-chain transactions where the storage can be modified. We call them mutable calls. They has the same effect as the vanilla ink!. On the other hand, it supports complex and async computation in queries (immutable calls), like what we have described above.

Can't you split this up in two contracts? One has the storage (A) and mutable calls but no floats and can be easily run on-chain. Then you have another one (B) which contains floats and is responsible for the calculation.

In case of off-chain calculation B would query all the data from A by calling it and then does its calculation on it. It doesn't matter if a lot of data is dumped from A to B because it only happens off-chain.

This would remove the dynamic requirement and makes this easily implementable.

@h4x3rotab
Copy link
Contributor

Brilliant solution! Let's do it in this way.

@kvinwang
Copy link
Contributor Author

kvinwang commented Aug 12, 2022

Can't you split this up in two contracts? One has the storage (A) and mutable calls but no floats and can be easily run on-chain. Then you have another one (B) which contains floats and is responsible for the calculation.

In case of off-chain calculation B would query all the data from A by calling it and then does its calculation on it. It doesn't matter if a lot of data is dumped from A to B because it only happens off-chain.

This would remove the dynamic requirement and makes this easily implementable.

The solution is acceptable to us.
However, I am worrying about whether it is easy to implement.
I think in this solution, If a contract B contains floats, it would be marked as non-deteministic, and further, the pallet-contracts should forbid it to run in transaction mode. But a contract would at least requires executing its constructor even if it contains floats. How should we deal with this issue?

Do I understand it correctly?

@athei
Copy link
Member

athei commented Aug 12, 2022

Yes you got that correctly. It is a good point: You can't create contract B cause you cannot run the constructor on-chain of a code that is flagged as in-deterministic.

There is a solution for that and I think this is even more elegant: You don't actually need a contract B. You only have a code hash B which is flagged as nondeterministic. What you would do is to use seal_delegate_call to run this code in the context of A. This way you don't even need to pass data from A to B. You can just read the storage of A with the code of B. It is totally fine that B can never be instantiated: You could never crate any storage anyways. Trying to call into B with seal_delegate_call would fail on-chain.

@kvinwang
Copy link
Contributor Author

There is a solution for that and I think this is even more elegant: You don't actually need a contract B. You only have a code hash B which is flagged as nondeterministic. What you would do is to use seal_delegate_call to run this code in the context of A. This way you don't even need to pass data from A to B. You can just read the storage of A with the code of B. It is totally fine that B can never be instantiated: You could never crate any storage anyways. Trying to call into B with seal_delegate_call would fail on-chain.

Perfect!

@athei
Copy link
Member

athei commented Aug 18, 2022

Replaced by #12063

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
J0-enhancement An additional feature request. Z2-medium Can be fixed by a coder with good Rust knowledge but little knowledge of the codebase.
Projects
Status: Done
Development

Successfully merging a pull request may close this issue.

5 participants