Create struct Program + EncodeToAny + DecodeFromAny#8
Create struct Program + EncodeToAny + DecodeFromAny#8
Conversation
src/data.rs
Outdated
|
|
||
| #[derive(Debug, Default, PartialEq)] | ||
| pub struct Program { | ||
| module: libernet::wasm::ProgramModule, |
There was a problem hiding this comment.
As it stands, the Default construction of a Program relies on the Default construction of a libernet::wasm::ProgramModule being "empty". Right now that happens to be fairly accurate because a default-constructed ProgramModule will have no sections and no version number, but this assumption kind of breaks an abstraction layer and I'd rather not rely on it.
For context: the default construction of anything stored in an SMT is important because it's used in phantom nodes to simulate a complete tree.
Better approach: let's wrap the ProgramModule in an Option. If it's None it means the SMT leaf doesn't have a program for that address.
| module: libernet::wasm::ProgramModule, | |
| module: Option<libernet::wasm::ProgramModule>, |
In the coming PRs, when it's time to hash the module with Poseidon, if the Option is None just return the zero scalar. That's safe because no one in the world knows a preimage for zero on a cryptographic hash, and that will be the case for the foreseeable future (in fact, sending coins to the zero address is also a common means to effectively burn them in the blockchain world).
There was a problem hiding this comment.
@71104 I didn’t quite understand how to implement EncodeToAny when the module is None. Should I still encode it, or should I return an error?
There was a problem hiding this comment.
That would happen when the user queries the code of a program that doesn't exist, so return an error in that case.
8770051 to
487b17c
Compare
|
|
||
| impl proto::EncodeToAny for Program { | ||
| fn encode_to_any(&self) -> Result<prost_types::Any> { | ||
| Ok(prost_types::Any::from_msg( |
There was a problem hiding this comment.
Ok(... ?) is redundant.
| Ok(prost_types::Any::from_msg( | |
| prost_types::Any::from_msg( | |
| self.module.as_ref().context("missing program module")?, | |
| ) |
| }}; | ||
| } | ||
|
|
||
| trait ToScalar { |
There was a problem hiding this comment.
This name needs to be more explicative.
| trait ToScalar { | |
| trait HashToScalar { |
There was a problem hiding this comment.
Just curious — why HashToScalar? The trait is applied to a structured object, not to a hash. It feels like a step in the chain is missing: SomeToHash → HashToScalar.
There was a problem hiding this comment.
In this case "hash" was meant as a verb, not a noun: "hash that thing to a scalar".
But after my last email we should actually rename this to Sha3Hash (and in that case "hash" is a noun).
| } | ||
|
|
||
| trait ToScalar { | ||
| fn to_scalar(&self) -> Result<Scalar>; |
There was a problem hiding this comment.
Similary, this should be renamed to hash_to_scalar.
| fn to_scalar(&self) -> Result<Scalar>; | |
| fn hash_to_scalar(&self) -> Result<Scalar>; |
There was a problem hiding this comment.
But, after my last email, it should actually be sha3_hash and have a SHA3 hasher argument:
| fn to_scalar(&self) -> Result<Scalar>; | |
| fn sha3_hash(&self, hasher: &mut sha3::Digest) -> Result<()>; |
| } | ||
| } | ||
|
|
||
| impl<T> ToScalar for Vec<T> |
There was a problem hiding this comment.
Nitpick: throughout this code base I've never used the where syntax because I find it needlessly verbose. I prefer this syntax:
| impl<T> ToScalar for Vec<T> | |
| impl<T: HashToScalar> HashToScalar for Vec<T> { |
| fn to_scalar(&self) -> Result<Scalar> { | ||
| let value_code = self | ||
| .value_type | ||
| .ok_or_else(|| anyhow!("Value type is required"))?; |
There was a problem hiding this comment.
You can use context whenever you need to convert an Option to a Result.
| .ok_or_else(|| anyhow!("Value type is required"))?; | |
| .context("Value type is required")?; |
| if self.reference_type.is_some() { | ||
| bail!("Reference type is set for primitive value type"); | ||
| } | ||
| 0.as_scalar() |
There was a problem hiding this comment.
When we need to construct scalars from integers it's better to rely on the From<u64> trait rather than our own AsScalar because the latter doesn't explicitly specify how the value is rendered in the output scalar: it just so happens to be the integer itself for integer types, but it's a hash for other types. By contrast, From<u64> explicitly tells me you're using the integer value to initialize the output scalar.
| 0.as_scalar() | |
| 0.into() |
| .value_type | ||
| .ok_or_else(|| anyhow!("Value type is required"))?; | ||
| let plain_type = PlainType::try_from(value_code)?; | ||
| let body = match plain_type { |
There was a problem hiding this comment.
Better to type this explicitly because if some of the branches return into() (as per my suggestion below) the type might be ambiguous.
| let body = match plain_type { | |
| let body: Scalar = match plain_type { |
| PlainType::ValueTypeRef => { | ||
| let ref_code = self | ||
| .reference_type | ||
| .ok_or_else(|| anyhow!("Reference type is required"))?; |
There was a problem hiding this comment.
| .ok_or_else(|| anyhow!("Reference type is required"))?; | |
| .context("Reference type is required")?; |
| impl ToScalar for wasm::block_type::BlockType { | ||
| fn to_scalar(&self) -> Result<Scalar> { | ||
| Ok(match self { | ||
| wasm::block_type::BlockType::Empty(_) => -1.as_scalar(), |
There was a problem hiding this comment.
| wasm::block_type::BlockType::Empty(_) => -1.as_scalar(), | |
| wasm::block_type::BlockType::Empty(_) => -Scalar::from(1), |
| Ok(match self { | ||
| wasm::block_type::BlockType::Empty(_) => -1.as_scalar(), | ||
| wasm::block_type::BlockType::ValueType(vt) => vt.to_scalar()?, | ||
| wasm::block_type::BlockType::FunctionType(v) => v.as_scalar(), |
There was a problem hiding this comment.
I should have probably caught this before but... why do we have this FunctionType variant here, inside BlockType? The specs say a block type is either a "value type" or a "type index".
Similarly, why do we have the Empty variant?
There was a problem hiding this comment.
FuncType is a typeidx (an index into the module’s type section). The type section contains only function types, so it always resolves to a functype. I used wasmparser’s terminology to keep round-trip conversion straightforward, but we can rename it if we want to align more closely with the spec.
There was a problem hiding this comment.
Regarding Empty, I followed the wasmparser approach. As I understand it, the library tries to stay as close as possible to the WebAssembly binary format. In the binary encoding of blocktype, an empty block is represented by the constant 0x40. This special encoding exists to keep the binary smaller, since blocks without results are very common.
There was a problem hiding this comment.
Alright, let's leave the Empty variant there but the specs really seem to say that function index and type index are two different things (two different types of indices), and block types use type indices: https://webassembly.github.io/spec/core/syntax/types.html#syntax-blocktype
typeidx links to the full list of index types: https://webassembly.github.io/spec/core/syntax/modules.html#syntax-typeidx
The name FunctionType we're using right now sounds a lot like we're using a "funcidx" from the specs.
AIUI, this object describes the type returned from within a block of statements that doesn't necessarily represent a function body, it could be eg. an if branch or its else branch (is that right?). In fact, here it is in several instances: https://webassembly.github.io/spec/core/syntax/instructions.html#syntax-instr-control
| fn to_scalar(&self) -> Result<Scalar> { | ||
| let catch_element = self | ||
| .catch_element | ||
| .ok_or(anyhow!("Catch element is required"))?; |
There was a problem hiding this comment.
Use context here and everywhere in the following please.
| .ok_or(anyhow!("Catch element is required"))?; | |
| .context("Catch element is required")?; |
|
|
||
| #[derive(Debug, Default)] | ||
| #[cfg_attr(test, derive(PartialEq))] | ||
| pub struct Program { |
There was a problem hiding this comment.
Since hashing the WASM AST is relatively expensive and Program objects must be immutable anyway, let's compute the hash upon construction and cache it in a field!
Example:
#[derive(Debug, Default)]
pub struct Program {
module: Option<libernet::wasm::ProgramModule>,
hash: Scalar,
}
impl Program {
pub fn new(module: libernet::wasm::ProgramModule) -> Self {
let hash = module.as_scalar();
Self {
module: Some(module),
hash
}
}
}
impl AsScalar for Program {
fn hash(&self) -> Scalar {
self.hash
}
}The Default construction of a Scalar returns the zero scalar so that works.
No description provided.