-
Notifications
You must be signed in to change notification settings - Fork 1.7k
WASM contracts MVP #5679
WASM contracts MVP #5679
Conversation
ethcore/Cargo.toml
Outdated
@@ -51,6 +51,8 @@ semver = "0.6" | |||
stats = { path = "../util/stats" } | |||
time = "0.1" | |||
transient-hashmap = "0.4" | |||
parity-wasm = { git = "http://github.com/nikvolf/parity-wasm" } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should use https
for that.
ethcore/src/evm/wasm/mod.rs
Outdated
interpreter::UserFunctions { | ||
executor: runtime, | ||
functions: vec![ | ||
interpreter::UserFunction { | ||
name: "_storage_read".to_owned(), | ||
params: vec![elements::ValueType::I32, elements::ValueType::I32], | ||
result: Some(elements::ValueType::I32), | ||
params: vec![I32, I32], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
since these all seem to be statically known, why not have it take a &'static [ValueType]
instead of requiring allocation on each one?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, this optimisation can be made right away
@@ -66,6 +67,8 @@ pub enum Error { | |||
MutableCallInStaticContext, | |||
/// Likely to cause consensus issues. | |||
Internal(String), | |||
/// Wasm runtime error | |||
Wasm(String), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there something better we can give here than String
? Maybe a WasmError
enum with some number of "proper" error variants and one Unknown(String)
variant.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would avoid to wrap parity_wasm::interpreter::Error
, because it cannot be later meaningfully encoded as trace error and put in trace db, that's why it's encoded as string
Also I cannot imagine any code that would rely on handling specific wasm interpreter errors
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As long as it's strictly for human-readable messages then that's fine. If we ever finding ourself matching on it we can refactor to make it an enum.
} | ||
} | ||
|
||
fn native_bindings<'a>(runtime: &'a mut Runtime) -> interpreter::UserFunctions<'a> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unnecessary lifetime parameters, you can just write
fn native_bindings(runtime: &mut Runtime) -> interpreter::UserFunctions {
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not working, btw, since interpreter::UserFunctions
has an explicit lifetime parameter
see https://gitlab.parity.io/parity/parity/-/jobs/59538
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not because UserFunctions
has a lifetime parameter, it's because Runtime
has a lifetime parameter. &mut Runtime
has two elided lifetime parameters, so when you had the lifetime parameter to the function it elided one of them and not the other. You do need the parameters in this case, sorry. I didn't know Runtime
took a lifetime parameter.
Basically, it desugars to:
fn native_bindings<'a, 'b>(runtime: &'a mut Runtime<'b>) -> interpreter::UserFunctions<'???>
Since there are two parameters to choose from, it doesn't know which to pass to UserFunctions
.
Also, I think that the b"\0asm"
change will fail, since b"\0asm"
has type &[u8; 4]
. You need to do *b"\0asm"
It's easy to miss, I should have explicitly pointed it out in my original comment.
} | ||
} | ||
|
||
fn h256_at(&self, ptr: WasmPtr) -> Result<H256, interpreter::Error> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can this lead to type confusion bugs? Can we have a WasmPtr
carry a PhantomData
to ensure that we deserialize to the correct type? If not, can we mark these functions as unsafe
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's perfectly safe, because it works on bound-checked memory api
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bound-checked, sure, but that doesn't mean that it won't cause type confusion. We're for all intents and purposes doing unchecked mem::transmute
to synthesise these values. It's not going to access uninitialized memory but it may cause us to interpret data as the wrong type.
self.memory.set(d_ptr, &d_buf)?; | ||
|
||
// write call args to memory | ||
self.memory.set(args_ptr, &call_args.address)?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hard-coded byte offsets here - can we use mem::size_of_value
or abstract this some other way?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, it's crossplatform offset, we cannot rely on Rust memory layout, despite it may happen to have same value.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, can we at least extract it to be more self-documenting, or use debug_assert_eq!(mem::size_of_value(something), 20)
to make sure that we're not truncating data? Also, we can use non-target-dependent datatypes in a #[repr(C)]
struct, which also has the benefit of allowing us to blit it to memory in a single call (even if it requires doing mem::transmute::<Foo, [u8; 100]>(my_struct)
or whatever).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok, let's chat about this and WasmPtr
later in more detail
I will update in following prs if the chat will produce some useful outcome
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds like a great idea. Don't get me wrong, the code looks good, I'm just pointing to some areas that might deserve a second look. There's nothing that I would argue against merging because of.
"_debug" => { | ||
self.debug_log(context) | ||
}, | ||
"gas" => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this have an underscore like the rest? (I don't know, but it just sticks out so I thought I'd mention it).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we'd better keep it this way for now
to distinguish injected functions from runtime
"gas" function is unique in a way it cannot be called ever directly
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What happens if you compile a C file with a function called gas
to wasm and run it as a contract? If we want it to be impossible (for some value of "impossible") to accidentally link a file with a conflicting name we can use non-ASCII or unprintable characters, like \0
or 💩.
Obviously if defining gas
and compiling to wasm does nothing then you can just give it whatever name you like, I don't actually know the semantics here I'm just pointing out something that might cause a problem.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You cannot have named functions in webassembly, only imports or exports are named
And if C program will import gas
function explicitly, it will be invalid as a contract and will fail instantly, before even running (as wasm module validation fails when there are duplicated imports, there should be rule about it in webassembly official testsuite)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sweet, LGTM then.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Still worth adding a explicit test for this case, thanks
ethcore/src/executive.rs
Outdated
@@ -34,6 +34,8 @@ pub use types::executed::{Executed, ExecutionResult}; | |||
/// Maybe something like here: `https://github.com/ethereum/libethereum/blob/4db169b8504f2b87f7d5a481819cfb959fc65f6c/libethereum/ExtVM.cpp` | |||
const STACK_SIZE_PER_DEPTH: usize = 24*1024; | |||
|
|||
const WASM_MAGIC_NUMBER: [u8; 4] = [0x00, 0x61, 0x73, 0x6d]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is just personal taste but if a byte array is supposed to be ASCII I'd rather write this as *b"\0asm"
instead. It doesn't matter, but it might be something to keep in mind going forward.
ethcore/src/executive.rs
Outdated
@@ -34,7 +34,7 @@ pub use types::executed::{Executed, ExecutionResult}; | |||
/// Maybe something like here: `https://github.com/ethereum/libethereum/blob/4db169b8504f2b87f7d5a481819cfb959fc65f6c/libethereum/ExtVM.cpp` | |||
const STACK_SIZE_PER_DEPTH: usize = 24*1024; | |||
|
|||
const WASM_MAGIC_NUMBER: [u8; 4] = b"\0asm"; | |||
const WASM_MAGIC_NUMBER: &'static [u8; 4] = b"\0asm"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alternatively (it doesn't matter in this case, but it might in the future):
const WASM_MAGIC_NUMBER: [u8; 4] = *b"\0asm";
Note the deref operator in the RHS. Also, Rust will automatically infer 'static
in const
and static
items (so you can write &[u8; 4]
for the type).
If wasm contracts are enabled by the engine, evm executor will choose wasm interpreter to run the code if also the contract code starts with wasm program header.
Wasm contracts can be called, but cannot call other contracts in the MVP
Wasm contracts can suicide
Wasm contracts can read and update storage, allocate memory and use most of the rust std lib (the only implication of it is the resulting size of the contract)
Build process for the contracts and use of the state api both can be found in
wasm-tests
repo:https://github.com/paritytech/wasm-tests (the parity also runs tests against them, compiled). Rust wasm support is not very mature still, requires nightly and some linker args magic to produce result we need (contributions and suggestions to improve the above are welcome)
parity-wasm
repo is the general purpose crate to serialize/deserialize wasm and also hosts (probably temporary) the interpreterwasm-utils
repo contains utilities to manipulate wasm for parity-specific targets (optimize symbols, substitute allocators, inject gas counter)