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

Provide support for solidity in serde_generate #61

Open
wants to merge 59 commits into
base: main
Choose a base branch
from

Conversation

MathieuDutSik
Copy link
Contributor

Summary

Solidity is the Javascript of blockchains, therefore having a way to support solidity in one of the codes is quite important.

Solidity has a number of limitations that force some adaptations:

  • The only enum supported are of the elementary types, therefore, the more complex enums are supported as a "choice" entry in the code.
  • There is no Tuple, so again a struct is introduced.
  • There is no Option type, therefore, we again introduce a struct construction.
  • The TupleArray [T; N] are also encoded as struct with the length being part of the typename.

There are also some more mundane restrictions where we choose to simply panic with an explicit error if this occurs in the input code:

  • No floating point.
  • No empty struct
  • No empty enum
  • Enum with at most 256 entries.
  • No recursive structure like trees.

The main implication of the use of struct to represent nested structure is that it breaks the design of ContainerFormat/ Format. So, we need to introduce the recursive type SolFormat type that provides support for this.

Other design choices:

  • No runtime is used. There is a way to have libraries in solidity, but it complicates the development since the library needs to be deployed first followed by the other contract.
  • Everything uses the "memory" location. I tried to use the more efficient "calldata" without success. Not sure if that is possible.
  • The get_keywords is a standard function instead of being a static one as in the OCaml generator. I think it does not matter.
  • Solidity has the abi.encode / abi.decode that works with padded serialization. It has an abi.encodePacked that provides a compact serialization, but it has no abi.decodePacked. Added to that the BCS serialization of 42_u32 is [42,0,0,0] but the solidity serialization is [0,0,0,42]. Therefore hand-crafted code has been written for processing all primitive types.
  • No assembly is used for the operation. There is room for improvement here.

Test Plan

The code is tested by running an EVM virtual machine. This is nicely provided by the REVM crate.
The only external files used are the temporary ones created during the test which makes the development smooth.

serde-generate-bin/tests/cli.rs Show resolved Hide resolved
serde-generate/src/solidity.rs Outdated Show resolved Hide resolved
}
}

fn need_memory(sol_format: &SolFormat, sol_registry: &SolRegistry) -> bool {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we make that a method of either SolRegistry or SolFormat?

serde-generate/src/solidity.rs Outdated Show resolved Hide resolved
serde-generate/src/solidity.rs Outdated Show resolved Hide resolved
serde-generate/src/solidity.rs Outdated Show resolved Hide resolved
serde-generate/src/solidity.rs Outdated Show resolved Hide resolved
serde-generate/tests/solidity_runtime.rs Outdated Show resolved Hide resolved
}
}

pub fn output<T: std::io::Write>(&self, out: &mut IndentedWriter<T>) -> Result<()> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Probably ok for a first version but I'm assuming re-using a library would cheaper in terms of gas?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think the opposite:

  • When we have all the source code in one single file, then the LLVM intermediate representation is fully available therefore optimizations are available.
  • When a library is available, then the linking occurs when the library is deployed. This of course prevents some optimization. I think it has its uses if the library is an external one. However, we do not gain anything by having a library.

Copy link
Contributor

Choose a reason for hiding this comment

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

We can add an option later.

@ma2bd
Copy link
Contributor

ma2bd commented Jan 17, 2025

#62 should help with CI. Let's see if it comes back green.

@MathieuDutSik MathieuDutSik force-pushed the serde_generate_solidity branch from f1c98ad to 3dec9d1 Compare January 18, 2025 13:08
serde-reflection/src/lib.rs Outdated Show resolved Hide resolved
rust-toolchain.toml Outdated Show resolved Hide resolved
.github/workflows/main.yml Outdated Show resolved Hide resolved
rust-toolchain.toml Outdated Show resolved Hide resolved
@MathieuDutSik MathieuDutSik force-pushed the serde_generate_solidity branch from e25765b to a2b5f4a Compare January 29, 2025 08:00
generator.output(&mut test_file, &registry).unwrap();
}

let _bytecode = get_bytecode(path, "test.sol", "test").unwrap();
Copy link
Contributor

Choose a reason for hiding this comment

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

let _bytecode = is not necessarily (and slightly counter-productive. E.g. it disables the warning that would be triggered if you forget the .unwrap() here).

Comment on lines 31 to 37
let ExecutionResult::Success {
reason: _,
gas_used: _,
gas_refunded: _,
logs: _,
output,
} = result
Copy link
Contributor

Choose a reason for hiding this comment

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

let ExecutionResult::Success { output, .. } = result is probably fine, especially for testing.

output: _,
} = result
else {
panic!("The TxKind::Call execution failed to be done");
Copy link
Contributor

Choose a reason for hiding this comment

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

remove "to be done"

impl SolRegistry {
fn insert(&mut self, sol_format: SolFormat) {
let key_name = sol_format.key_name();
if matches!(sol_format, SolFormat::Primitive(Primitive::I8)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

The cases look disjoint. Why not match sol_format { .. } ?

Comment on lines 859 to 865
_ => {}
}
// Typename entries are by definition already covered and do not need
// to be inserted. Others do.
if !matches!(sol_format, SolFormat::TypeName(_)) {
self.names.insert(key_name, sol_format);
}
Copy link
Contributor

@ma2bd ma2bd Jan 30, 2025

Choose a reason for hiding this comment

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

There's really no need for matches! here.

SolFormat::Primitive(SolFormat::TypeName(_)) => {
    // Typename entries do not need to be inserted.
}
_ => {
    self.names.insert(key_name, sol_format);
}

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