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

Codec & serializer - initial implementation #1

Merged
merged 24 commits into from
May 7, 2024
Merged

Codec & serializer - initial implementation #1

merged 24 commits into from
May 7, 2024

Conversation

andreibancioiu
Copy link
Contributor

@andreibancioiu andreibancioiu commented Apr 10, 2024

Initial implementation for codecs & serializer:

Usage examples are best visible in the file abi/serializer_test.go.

Not yet implemented: token identifier, arrays, tuples, counted variadic. Will be implemented in the next PR.

abi/valuesSingle.go Outdated Show resolved Hide resolved
abi/valuesSingle.go Outdated Show resolved Hide resolved
"io"
)

func (c *codec) encodeNestedList(writer io.Writer, value InputListValue) error {

Choose a reason for hiding this comment

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

I personally do not like splitting a large struct in more files
This could have been another independent struct that the codec struct could have extend it.
Valid for all other files containing specialized codec methods

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Indeed. Change in progress.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Refactored, using separate structs now. Using regular composition, though, instead of go-like composition. Some codecs rely on the general codec to perform their tasks.

I've postponed the renaming of the files, so that this fix-after-review doesn't bring a big delta: #3.


func (c *codec) encodeNestedOption(writer io.Writer, value OptionValue) error {
if value.Value == nil {
_, err := writer.Write([]byte{0})

Choose a reason for hiding this comment

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

0 & 1 could have been defined constants

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed, using constants.

return bigIntFromBytes(data, withSign)
}

func bigIntToBytes(value *big.Int, withSign bool) []byte {

Choose a reason for hiding this comment

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

I would remove the withSign bool and since this function will become 2 one-liner functions they could be dropped. Then, moving upstream, the functions from this file will be doubled (of course extracting out the common code) but we will get rid of a bool which I find ugly.
Of course, just my opinion, the code is maintainable as it is, and I'm pretty sure it works.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Indeed, refactored.

abi/parts.go Outdated
}

func (holder *partsHolder) appendEmptyPart() {
holder.parts = append(holder.parts, []byte{})

Choose a reason for hiding this comment

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

weak warning when using empty literal declaration. Switch to make([]byte, 0) ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Adjusted.

err = s.serializeInputMultiValue(partsHolder, value)
case InputVariadicValues:
if i != len(inputValues)-1 {
return errors.New("variadic values must be last among input values")

Choose a reason for hiding this comment

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

👍

abi/serializer_test.go Show resolved Hide resolved
abi/shared.go Outdated

func readBytesExactly(reader io.Reader, numBytes int) ([]byte, error) {
if numBytes == 0 {
return []byte{}, nil

Choose a reason for hiding this comment

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

return make([]byte, 0), nil ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed.

import "math/big"

// U8Value is a wrapper for uint8
type U8Value struct {

Choose a reason for hiding this comment

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

question: would have been better if we would have avoided these wrapper structs and instead used alias types?

Copy link
Contributor Author

@andreibancioiu andreibancioiu Apr 29, 2024

Choose a reason for hiding this comment

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

Will attempt to do something like that for primitives (pending).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For encoding, we could have passed primitive Go values - simply passed by value.

For decoding, we had two choices: either require the caller to pass a (mutable) pointer towards the (empty) primitive placeholder, or to pass a reference of a wrapper.

In JavaScript / Python etc., passing mutable pointers is not an option. And in order to have some consistency for the codecs, across multiple languages, the wrapper-based solution seemed a good option.

However, indeed, we should strive to support primitive values as much as possible, as well. I've added an issue here: #4.

Is it OK if we postpone this (somehow larger change) to a next PR?

Choose a reason for hiding this comment

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

Sure, can be postponed. Not a blocker issue

}

codec.codecForList = &codecForList{
generalCodec: codec,
Copy link

@iulianpascalau iulianpascalau Apr 30, 2024

Choose a reason for hiding this comment

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

well, this did not turn out as expected. This is actually worse than the initial implementation, Why? We now have reciprocal pointer links which should always be avoided.
Can we move into a new baseCoded the general functionality from the codec?
Or at least, have an unexported interface instead of clear pointer.

Copy link
Contributor Author

@andreibancioiu andreibancioiu Apr 30, 2024

Choose a reason for hiding this comment

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

Applied the second suggestion (depend on the general codec by means of an interface).

There isn't a "base functionality" per-se that can be extracted to a base codec. That is, some "leaf" codecs (e.g. the one for lists of items) depend on all the other codecs at runtime (to be able to handle all types of items within the list).

Another approach (with a bigger refactoring, though) would be to attach the encoding and decoding functionality to the value structs themselves. That way, we would be able to apply the composite pattern as follows (example):

struct ListValue:
    encodeNested()
        for each item in the list, call item.encodeNested()
    ...
    
struct U64Value:
    encodeNested():
        ...

list = new ListValue([new U64Value(42), new U64Value(43)])
list.encodeNested(...)

However, in this way, we would couple the dummy wrapper values with encoding / decoding logic. E.g. if we decide upon a different serialization format (though chances are slim), a separate family of structs will be required e.g. ListValueV3, OptionValueV3. This would also be possible to overcome using factories - of families of structs (with a given encoding / decoding specification), but that would somehow complicate a bit the design.

What is your opinion? All right as now, using an internal interface for generalCodec - or should we switch to another design (e.g. the one in the paragraph above)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Re-re-thinking about it - perhaps having moved the encoding / decoding logic in the value structs themselves, and have them implement the Encodable interface (or something like that) - with 4 functions (encode nested, encode top level, decode nested, decode top level) - does not sound that bad anymore.

Choose a reason for hiding this comment

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

That is exactly what we have with the trie elements: leafNode, extensionNode, branchNode. The "child"/"children" node(s) is always an interface that can be either implementation

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks. Added a GH issue here for this topic: #5

@andreibancioiu andreibancioiu merged commit 2d1a95c into main May 7, 2024
3 checks passed
@andreibancioiu andreibancioiu deleted the init branch May 7, 2024 07:17
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.

4 participants