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

accounts/abi: fix event tupleUnpack errors + cosmetic cleaning #15452

Closed
wants to merge 6 commits into from

Conversation

robert-zaremba
Copy link
Contributor

The fix commits contain fixes for two bugs in event unpacker:

  1. Not ignoring Indexed arguments when computing data offset
  2. Wrong expected slice/array size
  3. Excessive value extraction from the slice element (.Elem method) - which caused panic in tests.

The tests commit contains tests for all cases. For convenience I've added new dependency for doing assertions in tests (github.com/stretchr/testify).

Last commit contains cosmetic cleanup.

@GitCop
Copy link

GitCop commented Nov 10, 2017

Thank you for your contribution! Your commits seem to not adhere to the repository coding standards

  • Commit: 5b4992b
  • Your commit message body contains a line that is longer than 80 characters

Please check the contribution guidelines for more details.


This message was auto-generated by https://gitcop.com

@GitCop
Copy link

GitCop commented Nov 10, 2017

Thank you for your contribution! Your commits seem to not adhere to the repository coding standards

  • Commit: 5b4992b
  • Your commit message body contains a line that is longer than 80 characters

Please check the contribution guidelines for more details.


This message was auto-generated by https://gitcop.com

Copy link
Member

@gballet gballet left a comment

Choose a reason for hiding this comment

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

Please revert your "cosmetic changes" as they clutter the patch. For the same reason, remove the dependency.
Also, explain exactly what tests panic when calling Elem(). Last, is there an associated issue that describe how wrong the array size is? If not, it wouldn't hurt describing what goes wrong as well.

typ = value.Type()
value = valueOf.Elem()
typ = value.Type()
isStruct = true
Copy link
Member

Choose a reason for hiding this comment

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

It would make more sense to set isStruct to false and only set it to true if value.Kind() is a reflect.Struct. If that switch starts supporting more types, the programmer will have to set isStruct = false on each entry (and might forget)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

good point. The only additional type which we could support here is map, and this can be easily handled with enum type, which we will define once (performance, checks...)

)
switch value.Kind() {
case reflect.Struct:
Copy link
Member

Choose a reason for hiding this comment

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

Another reason to initialize isStruct to true would be to not have an empty case here

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

@@ -85,7 +85,7 @@ func (abi ABI) Unpack(v interface{}, name string, output []byte) (err error) {
} else if event, ok := abi.Events[name]; ok {
unpack = event
} else {
return fmt.Errorf("abi: could not locate named method or event.")
return fmt.Errorf("abi: could not locate named method or event")
Copy link
Member

Choose a reason for hiding this comment

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

Is that change really necessary? I can see the point (pun intended) of not displaying two dots if that error is subsequently printed as a %v followed by another dot. However, it would be preferable not to add that dot over there, and not clutter the diff with unnecessary entries (more on that later)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was reported by golinit

marshalledValue, err := toGoType((i+j)*32, input.Type, output)
if err != nil {
return err
}
reflectValue := reflect.ValueOf(marshalledValue)

switch value.Kind() {
case reflect.Struct:
if isStruct {
Copy link
Member

Choose a reason for hiding this comment

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

The switch made perfect sense here (especially since you are also merely moving it), as other types can be supported in the future.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

as explained below - we should check the type before unpacking the arguments in order to validate the array size once and for all. We can do the switch here, but since we already did it it's easier to reuse what we have learned.

Copy link
Member

Choose a reason for hiding this comment

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

The second switch will be optimized by the compiler, and we want to make adding new types easier. Revert back to it, please.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ok

return err
}
}
}
case reflect.Slice, reflect.Array:
if value.Len() < i {
Copy link
Member

Choose a reason for hiding this comment

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

You could move the checks from line 68 right here, for a reduced diff and better understanding

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The slice / array size should be done once, and at the beginning. Not every time in the loop. Also the error message was wrong, because it could fail before reaching the end (reporting the wrong expected size).

Copy link
Member

Choose a reason for hiding this comment

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

Or you could let the loop unroll with a i++ followed by a continue and make the size check at the end. It would mean that you only scan the argument list once, and then that you would report other errors earlier than an error on the length of the array (which is unlikely to occur). Do let me know what you think of this, I might be missing something out here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Why you want to do the check at the end? We can do both things in fact: to use switch before the unpacking to verify preconditions, and the use switch here as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Furthermore if we will not change the size of slice before setting the value we will make a panic when we will go out of the slice / array.
BTW: the previous behaviour (few weeks ago) was a bit better in this case because it was specific to handle the slice/array use case (the tradeof was to have multiple copies of for loops for each destination value kind case). Also it handled the case where the slice was nil, and was appending the values.

"strings"
"testing"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/stretchr/testify/assert"
Copy link
Member

Choose a reason for hiding this comment

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

You're pulling in two libraries that produce a big diff (and code) size, and you're the only one using it. I suggest that you create a different PR to start using those two libraries, so that the content of this PR is only about tupleUnpack. If the community is interested in using testify, you will get upvotes and we'll then include it. This PR does too many different things and we can't include it whole.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

OK, will do that.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Testify is a really convenient library when doing tests. I will extract it from this pull request and do another one then.

@@ -192,6 +194,8 @@ func (m Method) String() string {
return fmt.Sprintf("function %v(%v) %sreturns(%v)", m.Name, strings.Join(inputs, ", "), constant, strings.Join(outputs, ", "))
}

// Id returns the canonical representation of the method's signature used by the
// abi definition to identify method name.
Copy link
Member

Choose a reason for hiding this comment

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

identify THE method name

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated, thanks.

@GitCop
Copy link

GitCop commented Nov 10, 2017

Thank you for your contribution! Your commits seem to not adhere to the repository coding standards

  • Commit: 9cb7455

  • Commits must be prefixed with the package(s) they modify

  • Commit: 5a20f16

  • Commits must be prefixed with the package(s) they modify

Please check the contribution guidelines for more details.


This message was auto-generated by https://gitcop.com

[3]byte{'u', 's', 'd'}},
jsonEventPledge,
"Can unpack Pledge event into slice",
}}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

All the Slice based tests were failing because of the Elem() call in tupleUnpack:

  • Can unpack ERC20 Transfer event into slice
  • Can unpack Pledge event into slice

@robert-zaremba
Copy link
Contributor Author

@gballet Thanks for review. Please look at my answers above.

  • I've updated isStruct initialization
  • in the comment I've pointed out which tests are failing because of Elem() call
  • removed the testify dependency and updated tests accordingly to that
  • I kept the cosmetic update because it's not that big and solve most of the linter warnings. It was easier for me to do it while working on this package.

Please confirm everything is OK, and I can squash the commits before merging the PR.

@robert-zaremba
Copy link
Contributor Author

@gballet I've squashed the review changes. Each commit has the description explaining the changes (eg the Elem() call issue). It's much easier to review individual commits if you want to see the logical changes. As requested the testify dependency has been removed (with commit).

Copy link
Member

@gballet gballet left a comment

Choose a reason for hiding this comment

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

We're almost there. If you go back to switch and also add unit tests to cover your changes to methods.go, we should be good to go.

@@ -85,3 +85,10 @@ func set(dst, src reflect.Value, output Argument) error {
}
return nil
}

func requireAssignable(dst, src reflect.Value) error {
Copy link
Member

Choose a reason for hiding this comment

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

Please add a comment to explain what that function does, it's obvious to everyone familiar with this PR but it won't be down the road.

@@ -24,7 +24,7 @@ import (
"github.com/ethereum/go-ethereum/crypto"
)

// Callable method given a `Name` and whether the method is a constant.
// Method represents method ABI entry.
Copy link
Member

Choose a reason for hiding this comment

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

There's some information being lost here, as well as an article missing.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

comment should start with the object name. The thing about Name and constant is covered in next lines of the comment.
Updating to Method represents a method ...

marshalledValue, err := toGoType((i+j)*32, input.Type, output)
if err != nil {
return err
}
reflectValue := reflect.ValueOf(marshalledValue)

switch value.Kind() {
case reflect.Struct:
if isStruct {
Copy link
Member

Choose a reason for hiding this comment

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

The second switch will be optimized by the compiler, and we want to make adding new types easier. Revert back to it, please.

return err
}
}
}
case reflect.Slice, reflect.Array:
if value.Len() < i {
Copy link
Member

Choose a reason for hiding this comment

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

Or you could let the loop unroll with a i++ followed by a continue and make the size check at the end. It would mean that you only scan the argument list once, and then that you would report other errors earlier than an error on the length of the array (which is unlikely to occur). Do let me know what you think of this, I might be missing something out here.

v := value.Index(i)
if v.Kind() != reflect.Ptr && v.Kind() != reflect.Interface {
return fmt.Errorf("abi: cannot unmarshal %v in to %v", v.Type(), reflectValue.Type())
if err := requireAssignable(v, reflectValue); err != nil {
Copy link
Member

Choose a reason for hiding this comment

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

You're testing your changes in events.go, but not those in methods.go. The code is duplicated, and therefore in the future it could drift apart. So you need to cover that too.

Copy link
Member

@gballet gballet left a comment

Choose a reason for hiding this comment

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

Forgot

expected interface{}
jsonLog []byte
name string
}{{
Copy link
Member

Choose a reason for hiding this comment

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

Forgot to ask in my previous review: are you going to test for errors as well? Like, the array boundary check that you made some changes for?

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 can add tests for that (my goal was to quickly fix the bugs in this package which I encountered when using it)

@robert-zaremba
Copy link
Contributor Author

@gballet @karalabe I've updated the repo according to the review:

  • Added tests for Method slice unpack
  • researched the use-case for Elem: shortly it is there to be able to create a slice with elements we want to unpack to. Elem requires that we provide a slice of pointers into unpack. In my events test-cases I assumed that we we want to unpack directly into slice values. I brought back the Elem call and updated tests.

Copy link
Contributor Author

@robert-zaremba robert-zaremba left a comment

Choose a reason for hiding this comment

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

Done, we are using switch again.

marshalledValue, err := toGoType((i+j)*32, input.Type, output)
if err != nil {
return err
}
reflectValue := reflect.ValueOf(marshalledValue)

switch value.Kind() {
case reflect.Struct:
if isStruct {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

ok

return err
}
}
}
case reflect.Slice, reflect.Array:
if value.Len() < i {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Why you want to do the check at the end? We can do both things in fact: to use switch before the unpacking to verify preconditions, and the use switch here as well.

@@ -24,7 +24,7 @@ import (
"github.com/ethereum/go-ethereum/crypto"
)

// Callable method given a `Name` and whether the method is a constant.
// Method represents method ABI entry.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

comment should start with the object name. The thing about Name and constant is covered in next lines of the comment.
Updating to Method represents a method ...

expected interface{}
jsonLog []byte
name string
}{{
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 can add tests for that (my goal was to quickly fix the bugs in this package which I encountered when using it)

return err
}
}
}
case reflect.Slice, reflect.Array:
if value.Len() < i {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Furthermore if we will not change the size of slice before setting the value we will make a panic when we will go out of the slice / array.
BTW: the previous behaviour (few weeks ago) was a bit better in this case because it was specific to handle the slice/array use case (the tradeof was to have multiple copies of for loops for each destination value kind case). Also it handled the case where the slice was nil, and was appending the values.

@robert-zaremba
Copy link
Contributor Author

Also, since testify is merged I've updated (only) new tests to take advantage of it, which saved around 2 lines of code for each check :)

@robert-zaremba
Copy link
Contributor Author

Into the cleanning commit I've added one more golint complaint fix: unified Method method receiver name: Receiver names should be consistent across a type's methods.

Copy link
Member

@gballet gballet left a comment

Choose a reason for hiding this comment

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

We still have this issue with the Travis build, but it doesn't seem to be directly related. Let's not block this PR any longer and merge it.

@@ -24,7 +24,7 @@ import (
"github.com/ethereum/go-ethereum/crypto"
)

// Callable method given a `Name` and whether the method is a constant.
// Method represents a method ABI entry.
Copy link
Member

Choose a reason for hiding this comment

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

Your comment still drops information from the comment it replaced.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not really. Look at the whole comment. I think it's more clear now. The Name is part of it's public interface, and other part of the comment mentions about constant part.

Copy link
Member

Choose a reason for hiding this comment

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

No it definitely drops critical information regarding the ABI as the field "constant" is a core component of the current ABI. This will eventually be changed to "stateMutability" but for now it is needed, especially if the goal is to reduce breaking changes.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Well, the second line tells:

If the method is `Const` no transaction needs to be created for this

I don't want to insist here. I will update to:

Method represents a callable given a `Name` and whether the method is a constant.

j := 0
for i := 0; i < len(e.Inputs); i++ {
input := e.Inputs[i]
i, j := -1, 0
Copy link
Member

Choose a reason for hiding this comment

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

why is i -1 here? Is there any way to get a comment of some kind clarifying the logic here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Because we are incrementing at the beginning of the of the loop (if the argument is not Indexed) rather then at the end.

Copy link
Member

Choose a reason for hiding this comment

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

That doesn't seem to clarify things for me. So in the case of an event a(uint b, string indexed c, uint d), how does the -1 help read through b? Again, the logic could use some documentation is all I'm saying.

"indexed": false, "name": "currency", "type": "bytes3"
}],
"name": "Pledge",
"type": "event"
Copy link
Member

Choose a reason for hiding this comment

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

Needs a test to take into account static array types.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

@@ -85,3 +85,12 @@ func set(dst, src reflect.Value, output Argument) error {
}
return nil
}

// requireAssignable verifies that `dest` and `src` types are compatible
// and it's possible to safely assign `src` value to `dest`.
Copy link
Member

Choose a reason for hiding this comment

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

This does not appear to be what it's actually doing.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Right, there is better way to document it. Howe about:

// requireAssignable verifies that dest type is assignable.

Copy link
Member

Choose a reason for hiding this comment

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

That works better.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

changed to:

// requireAssignable assures that `dest` is a pointer and it's not an interface.

for i := 0; i < len(method.Outputs); i++ {
toUnpack := method.Outputs[i]
if toUnpack.Type.T == ArrayTy {
for i, o := range m.Outputs {
Copy link
Member

Choose a reason for hiding this comment

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

can we please use a more descriptive name than o?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This variable is used very often, and doesn't have a big context. But If you want I can change to something else.

continue
} else if input.Type.T == ArrayTy {
// need to move this up because they read sequentially
j += input.Type.Size
}
i++
Copy link
Member

Choose a reason for hiding this comment

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

Okay, I see the logic now, slightly...but this definitely needs documentation because currently it's a bit confusing as to why it's set up this way.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

OK, will add some comments.

@VoR0220
Copy link
Member

VoR0220 commented Dec 12, 2017

@dshulyak any issues?

@dshulyak
Copy link
Contributor

no issues, i will make separate pr with that bugfix or include it into this one #15618

@robert-zaremba
Copy link
Contributor Author

Great! I'm happy that we are finally moving forward. So

  • I'm removing last commit from this Pull Request (the static array argument fix). For reference it's available in the fix-static-array-event-argument branch
  • @dshulyak please add your change to this Pull Request (master branch in my repo) - I've just added you as a collaborator.
  • Should I remove my static-array test (it will fail without the last commit I've just removed from this PR).

Or please advice if you want to do differently?

@dshulyak
Copy link
Contributor

@robert-zaremba i would prefer to merge both PRs separately (my change is this one #15618), doesn't matter for me in which order.
and i think you can remove a test for array tuple unpack, cause it is not related to the original bugfix.

@robert-zaremba
Copy link
Contributor Author

@dshulyak OK. Removed.
Just in case, the ported fix + test is in fix-static-array-event-argument branch.

@VoR0220
Copy link
Member

VoR0220 commented Dec 13, 2017

@gballet @holiman @fjl @karalabe Can we get these two PRs merged up?

@robert-zaremba
Copy link
Contributor Author

Ping. Any update here?

Event.tupleUnpack doesn't handle correctly Indexed arguments,
hence it can't unpack an event with indexed arguments.
+ The event slice unpacker doesn't correctly extract element from the
slice. The indexed arguments are not ignored as they should be
(the data offset should not include the indexed arguments).

+ The `Elem()` call in the slice unpack doesn't work.
The Slice related tests fails because of that.

+ the check in the loop are suboptimal and have been extracted
out of the loop.

+ extracted common code from event and method tupleUnpack
+ adding missing comments
+ small cleanups which won't significantly change
  function body.
+ unify Method receiver name
+ Reworked Method Unpack tests into more readable components
+ Added Method Unpack into slice test
@holiman
Copy link
Contributor

holiman commented Dec 21, 2017

Did you just rebase this ?
I was just doing that locally aswell :)

@gballet
Copy link
Member

gballet commented Dec 21, 2017

yes I did, there seems to be a merge error though

@robert-zaremba
Copy link
Contributor Author

Let me know if you need some help with rebase or squashe. I didn't squash last commits because they were after the approved review.

@gballet
Copy link
Member

gballet commented Dec 21, 2017

that should be fine now, if tests pass then we're good to merge

holiman added a commit to holiman/go-ethereum that referenced this pull request Dec 21, 2017
@holiman holiman mentioned this pull request Dec 21, 2017
@robert-zaremba
Copy link
Contributor Author

It seams that there is an unrelated dependency build (installation) problem for GETH_ARCH=386, MSYS2_ARCH=i686, MSYS2_BITS=32, MSYSTEM=MINGW32

holiman added a commit to holiman/go-ethereum that referenced this pull request Dec 22, 2017
@holiman
Copy link
Contributor

holiman commented Dec 22, 2017

Closing due to #15731

@holiman holiman closed this Dec 22, 2017
mariameda pushed a commit to NiluPlatform/go-nilu that referenced this pull request Aug 23, 2018
hatricker pushed a commit to thundercore/thundercore-localchain that referenced this pull request Oct 8, 2018
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.

None yet

8 participants