Skip to content

Comments

Create struct Program + EncodeToAny + DecodeFromAny#8

Open
caiiiycuk wants to merge 6 commits intomainfrom
caiiiycuk-wasm
Open

Create struct Program + EncodeToAny + DecodeFromAny#8
caiiiycuk wants to merge 6 commits intomainfrom
caiiiycuk-wasm

Conversation

@caiiiycuk
Copy link
Collaborator

No description provided.

@caiiiycuk caiiiycuk requested a review from 71104 as a code owner February 9, 2026 13:30
src/data.rs Outdated

#[derive(Debug, Default, PartialEq)]
pub struct Program {
module: libernet::wasm::ProgramModule,
Copy link
Member

@71104 71104 Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would happen when the user queries the code of a program that doesn't exist, so return an error in that case.

Copy link
Member

@71104 71104 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mostly good.


impl proto::EncodeToAny for Program {
fn encode_to_any(&self) -> Result<prost_types::Any> {
Ok(prost_types::Any::from_msg(
Copy link
Member

@71104 71104 Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok(... ?) is redundant.

Suggested change
Ok(prost_types::Any::from_msg(
prost_types::Any::from_msg(
self.module.as_ref().context("missing program module")?,
)

}};
}

trait ToScalar {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This name needs to be more explicative.

Suggested change
trait ToScalar {
trait HashToScalar {

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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: SomeToHashHashToScalar.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similary, this should be renamed to hash_to_scalar.

Suggested change
fn to_scalar(&self) -> Result<Scalar>;
fn hash_to_scalar(&self) -> Result<Scalar>;

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But, after my last email, it should actually be sha3_hash and have a SHA3 hasher argument:

Suggested change
fn to_scalar(&self) -> Result<Scalar>;
fn sha3_hash(&self, hasher: &mut sha3::Digest) -> Result<()>;

}
}

impl<T> ToScalar for Vec<T>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: throughout this code base I've never used the where syntax because I find it needlessly verbose. I prefer this syntax:

Suggested change
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"))?;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use context whenever you need to convert an Option to a Result.

Suggested change
.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()
Copy link
Member

@71104 71104 Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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 {
Copy link
Member

@71104 71104 Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better to type this explicitly because if some of the branches return into() (as per my suggestion below) the type might be ambiguous.

Suggested change
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"))?;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.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(),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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(),
Copy link
Member

@71104 71104 Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

https://github.com/bytecodealliance/wasm-tools/blob/76927bf4bdbddf4b15f835c5eddfffbdfe3bdbd5/crates/wasmparser/src/readers/core/operators.rs#L21C1-L32C2

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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"))?;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use context here and everywhere in the following please.

Suggested change
.ok_or(anyhow!("Catch element is required"))?;
.context("Catch element is required")?;


#[derive(Debug, Default)]
#[cfg_attr(test, derive(PartialEq))]
pub struct Program {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants