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

[x/programs] Serialize params on the Rust side #979

Merged
merged 11 commits into from
Jun 3, 2024

Conversation

iFrostizz
Copy link
Contributor

Closes #927

Comment on lines -352 to -331
case float64:
// json unmarshal converts to float64
cp = append(cp, Parameter{Value: uint64(v), Type: param.Type})
case int:
if v < 0 {
return nil, fmt.Errorf("%w: %s", errors.New("negative value"), param.Type)
}
cp = append(cp, Parameter{Value: uint64(v), Type: param.Type})
case string:
number, err := strconv.ParseUint(v, 10, 64)
if err != nil {
return nil, fmt.Errorf("%w: %s", ErrFailedParamTypeCast, param.Type)
}
cp = append(cp, Parameter{Value: number, Type: param.Type})
Copy link
Contributor Author

Choose a reason for hiding this comment

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

we were not using this

Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm sure there's more unused code lurking too

@iFrostizz iFrostizz marked this pull request as ready for review May 29, 2024 22:14
dboehm-avalabs
dboehm-avalabs previously approved these changes May 30, 2024
Comment on lines 107 to 132
impl Serialize for Param {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut val = serializer.serialize_struct("param", 2)?;
match self {
Param::U64(num) => {
val.serialize_field("type", "u64")?;
val.serialize_field("value", &b64.encode(num.to_le_bytes()))?;
}
Param::String(text) => {
val.serialize_field("type", "string")?;
val.serialize_field("value", &b64.encode(text))?;
}
Param::Id(id) => {
val.serialize_field("type", "id")?;
let id = serde_json::to_vec(&id).map_err(serde::ser::Error::custom)?;
let id = &id[1..id.len() - 1]; // remove quotes
val.serialize_field("value", &b64.encode(id))?;
}
Param::Key(key) => {
let (key, algo) = match key {
Key::Ed25519(key) => (key, "ed25519"),
Key::Secp256r1(key) => (key, "secp256r1"),
};
val.serialize_field("type", algo)?;
val.serialize_field("value", &b64.encode(key))?;
}
}
val.end()
}
}
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 could not find a way to do all of this with the serde attribute macros in a nice way. It would probably be more verbose if we would have used #[serde(serialize_with = "...")] in order to support all of the variants, and it's more readable that way.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't quite understand why everything needs to be base64 encoded. If I had to guess, it's because it's easier to decode everything on the Go side, but you should just be able to switch on the type tag and only base64 decode where necessary. But IMO, it's an extra step that's unnecessary (and adds a little complexity).

Also, I think it would just be beneficial to use an intermediate type here:

#[derive(Serialize)]
#[serde(rename_all = "lowercase", tag = "type", content = "value")]
enum StringParam {
    U64(String),
    String(String),
    Id(String),
    #[serde(untagged)]
    Key(Key),
}

If you want, you can define the type inside the Serialize implementation, do a From conversion, then just serialize the intermediate type. This should all get optimized away by the compiler, especially when the type is only used in one place.

https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=755c35cea6729ce0da44df255dceeaed

Copy link
Contributor Author

@iFrostizz iFrostizz May 31, 2024

Choose a reason for hiding this comment

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

It's Go making choices for us when dealing with the []byte type, see https://pkg.go.dev/encoding/json#Marshal

Array and slice values encode as JSON arrays, except that []byte encodes as a base64-encoded string, and a nil slice encodes as the null JSON value.

I found online that people were forking the package to strip out the base64 part but I don't think that we should do that.

Copy link
Collaborator

@richardpringle richardpringle left a comment

Choose a reason for hiding this comment

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

Leaving this as a comment review since I'll need to re-approve anyway when the merge conflicts are resolved

x/programs/cmd/simulator/cmd/plan.go Outdated Show resolved Hide resolved
x/programs/cmd/simulator/cmd/plan.go Show resolved Hide resolved
Comment on lines -352 to -331
case float64:
// json unmarshal converts to float64
cp = append(cp, Parameter{Value: uint64(v), Type: param.Type})
case int:
if v < 0 {
return nil, fmt.Errorf("%w: %s", errors.New("negative value"), param.Type)
}
cp = append(cp, Parameter{Value: uint64(v), Type: param.Type})
case string:
number, err := strconv.ParseUint(v, 10, 64)
if err != nil {
return nil, fmt.Errorf("%w: %s", ErrFailedParamTypeCast, param.Type)
}
cp = append(cp, Parameter{Value: number, Type: param.Type})
Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm sure there's more unused code lurking too

x/programs/cmd/simulator/src/lib.rs Outdated Show resolved Hide resolved
x/programs/cmd/simulator/src/lib.rs Show resolved Hide resolved
Comment on lines 107 to 132
impl Serialize for Param {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut val = serializer.serialize_struct("param", 2)?;
match self {
Param::U64(num) => {
val.serialize_field("type", "u64")?;
val.serialize_field("value", &b64.encode(num.to_le_bytes()))?;
}
Param::String(text) => {
val.serialize_field("type", "string")?;
val.serialize_field("value", &b64.encode(text))?;
}
Param::Id(id) => {
val.serialize_field("type", "id")?;
let id = serde_json::to_vec(&id).map_err(serde::ser::Error::custom)?;
let id = &id[1..id.len() - 1]; // remove quotes
val.serialize_field("value", &b64.encode(id))?;
}
Param::Key(key) => {
let (key, algo) = match key {
Key::Ed25519(key) => (key, "ed25519"),
Key::Secp256r1(key) => (key, "secp256r1"),
};
val.serialize_field("type", algo)?;
val.serialize_field("value", &b64.encode(key))?;
}
}
val.end()
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

I don't quite understand why everything needs to be base64 encoded. If I had to guess, it's because it's easier to decode everything on the Go side, but you should just be able to switch on the type tag and only base64 decode where necessary. But IMO, it's an extra step that's unnecessary (and adds a little complexity).

Also, I think it would just be beneficial to use an intermediate type here:

#[derive(Serialize)]
#[serde(rename_all = "lowercase", tag = "type", content = "value")]
enum StringParam {
    U64(String),
    String(String),
    Id(String),
    #[serde(untagged)]
    Key(Key),
}

If you want, you can define the type inside the Serialize implementation, do a From conversion, then just serialize the intermediate type. This should all get optimized away by the compiler, especially when the type is only used in one place.

https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=755c35cea6729ce0da44df255dceeaed

x/programs/cmd/simulator/src/lib.rs Outdated Show resolved Hide resolved
x/programs/cmd/simulator/src/lib.rs Outdated Show resolved Hide resolved
x/programs/cmd/simulator/src/lib.rs Outdated Show resolved Hide resolved
@iFrostizz iFrostizz enabled auto-merge (squash) May 31, 2024 19:23
Copy link
Collaborator

@richardpringle richardpringle left a comment

Choose a reason for hiding this comment

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

All the base-64 stuff is a little convoluted. This is a good step into cleaning things up, but I think there's still more to do.

fn from(val: &'a Id) -> Self {
&val.0
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

👍

Comment on lines +116 to +131
{
match self {
Param::U64(num) => {
Serialize::serialize(&StringParam::U64(b64.encode(num.to_le_bytes())), serializer)
}
Param::String(text) => {
Serialize::serialize(&StringParam::String(b64.encode(text)), serializer)
}
Param::Id(id) => {
let num: &usize = id.into();
let id = format!("step_{}", num);
Serialize::serialize(&StringParam::Id(b64.encode(id)), serializer)
}
Param::Key(key) => Serialize::serialize(key, serializer),
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is a little more complex than what I had suggested. Also, you really don't need the step_. In json, the "type" field will be "id", right? that means that step_ is redundant.

@iFrostizz iFrostizz merged commit aae4c29 into main Jun 3, 2024
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Archived in project
Development

Successfully merging this pull request may close these issues.

[x/programs] Serialize params on the Rust side
3 participants