-
Notifications
You must be signed in to change notification settings - Fork 33
VDB-318 Allow for packed storage slots #121
Conversation
19ef7f2
to
ebfb496
Compare
376988a
to
b8e51a7
Compare
83a6c76
to
bf27858
Compare
bf27858
to
69ad521
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM - nice catch! 🥇
@@ -43,6 +47,11 @@ func decodeUint256(raw []byte) string { | |||
return n.String() | |||
} | |||
|
|||
func decodeUint128(raw []byte) string { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wondering if it might make sense to just have one decodeInteger
function as long as the implementation's the same for every type 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was thinking about that, and then also was wondering if it would make sense for each of these decode methods to do some sort of validation that the bytes passed in are the right length?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That seems like it would be cool, but do you know whether we can assume that input will be of a fixed length? Thinking that a uint128
could be 16 bytes if it's packed, or 32 if it's not. I suppose we could validate that the input is greater than the min required?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh yeah, that's a great point. validating a minimum requirement seems doable, but i'm not sure how useful that may be.
for now, i'll update this to have just one decodeInteger
method.
return getMetadata(name, keys, valueType, packedNames, packedTypes) | ||
} | ||
|
||
func getMetadata(name string, keys map[Key]string, t ValueType, packedNames map[int]string, packedTypes map[int]ValueType) StorageValueMetadata { | ||
return StorageValueMetadata{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is probably unnecessary but I'm wondering if we might want to put a guard here to check that if t == PackedSlot
then packedNames != nil
and packedTypes != nil
(and vice versa if t != PackedSlot
)
} | ||
|
||
func getNumberOfBytes(valueType ValueType) int { | ||
// 8 bits per byte |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe worth replacing with a var?
return decodedStorageSlotItems | ||
} | ||
|
||
func decodeIndividualItems(itemBytes []byte, valueType ValueType) string { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe this should be singular?
decodedStorageSlotItems := map[int]string{} | ||
numberOfTypes := len(packedTypes) | ||
|
||
for position := 0; position < numberOfTypes; position++ { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
could maybe replace this with for k, v := range packedTypes
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maps also don't have a guaranteed iteration order (golang actually purposefully randomizes the intersection order 😆: https://blog.golang.org/go-maps-in-action), so if we iterate over them with for k, v := range packedTypes
they won't necessarily decode the raw data in the right order.
I think an alternative to the current implementation could be to sort the packedTypes
keys, and then do something like this
var keys []int
for k := range packedTypes {
keys = append(keys, k)
}
sort.Ints(keys)
for _, k := range keys {
// similar loop
}
Do you think that's more readable?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point! I'm fine to stick with the current implementation given that we need a guaranteed iteration order.
I am wondering how much it would muck things up to try an implementation that doesn't require a specific iteration order. Thinking that perhaps we could implement a function that returns the relevant slice for a given index provided the indexes and types of all values packed into the slot, but I'm also not sure whether or not that would be more readable 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah, i need to think about this a bit more. 🤔 do you think it's worth adding a story in the back log to address this later?
Expect(repository.PassedMetadata).To(Equal(fakeMetadata)) | ||
expectedPassedValue := make(map[int]string) | ||
expectedPassedValue[0] = "10800" | ||
expectedPassedValue[1] = "172800" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know that 2a300
is "172800" in decimal and 2a30
is "10800", which is why you're setting these expectedPassedValue
s, but I don't understand how you know where to get the 2 slots from "000000000000000000000000000000000000000000000002a300000000002a30" for the expected values (or how the 0 padding works - is that in the solidity documentation?). Why is 2a30
the first slot and 2a300
the second? how do you know the second isn't 2a3000
edit: I commented on this before seeing the decodePackedSlot
function, which looks like it might help me understand
@@ -43,6 +47,11 @@ func decodeUint256(raw []byte) string { | |||
return n.String() | |||
} | |||
|
|||
func decodeUint128(raw []byte) string { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we have three decodeUintxx
functions that have the same implementation? Would it make sense to have a single decodeUint
function instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, that's a good point that maybe having just one decodeUint
function makes sense. The one thing I'm wondering is if having some sort of validation would make sense - like if we call decodeUint128
that we make sure that the passed in bytes are a uint128
. 🤔
} | ||
|
||
func getNumberOfBytes(valueType ValueType) int { | ||
// 8 bits per byte |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems like we should have a constant for the number 8 that makes it clear a byte is 8 bits (ie ByteBits
) - I imagine we use that a lot in the codebase. On the other hand that could be overkill and when I see computation/conversions with the number 8 I usually assume it's because it's the number of bits in a byte. 🤷♀
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i like the idea of having a constant
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is good. I'm having a hard time internalizing how the storage slots work, so I think I need to read the solidity documentation again.
@@ -51,3 +60,52 @@ func decodeUint48(raw []byte) string { | |||
func decodeAddress(raw []byte) string { | |||
return common.BytesToAddress(raw).Hex() | |||
} | |||
|
|||
func decodePackedSlot(raw []byte, packedTypes map[int]ValueType) map[int]string { | |||
storageSlotData := raw |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this assignment is superfluous; could name the argument storageSlotData
instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I assigned the raw
argument to storageSlotData
because then on line 84 we're updating what is in storageSlotData
by popping off the current item. Does that make sense?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh yeah that makes perfect sense, sorry I missed that!
- factor out bitsPerByte constant - panic if the necessary args aren't passed to GetStorageValueMetadataForPackedSlot - only have one decode integer method in the deocoder file
7c8337b
to
2c66afb
Compare
for storage values that are less than 32bytes the solidity compiler packs them into one storage slot, this PR starts to allow for that decoding