Skip to content

ABIEncoderV2: Implement calldata structs without dynamically encoded members. #5936

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

Merged
merged 1 commit into from
Feb 11, 2019

Conversation

ekpyron
Copy link
Member

@ekpyron ekpyron commented Feb 5, 2019

Part of #1603.

@ekpyron ekpyron requested a review from erak February 5, 2019 19:33
@codecov
Copy link

codecov bot commented Feb 5, 2019

Codecov Report

❗ No coverage uploaded for pull request base (develop@9460701). Click here to learn what that means.
The diff coverage is 93.45%.

Impacted file tree graph

@@            Coverage Diff             @@
##             develop    #5936   +/-   ##
==========================================
  Coverage           ?   88.37%           
==========================================
  Files              ?      359           
  Lines              ?    34309           
  Branches           ?     4064           
==========================================
  Hits               ?    30320           
  Misses             ?     2616           
  Partials           ?     1373
Flag Coverage Δ
#all 88.37% <93.45%> (?)
#syntax 27.93% <12.14%> (?)

@@ -827,6 +827,8 @@ class StructType: public ReferenceType
std::string richIdentifier() const override;
bool operator==(Type const& _other) const override;
unsigned calldataEncodedSize(bool _padded) const override;
// Offset of member in calldata. Does not work for recursive or dynamically encoded types.
unsigned calldataOffsetOfMember(std::string const& _name) const;
Copy link
Contributor

Choose a reason for hiding this comment

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

Wouldn't this better fit closer to memoryOffsetOfMember and storageOffsetOfMember?

{
if (structType->dataStoredIn(DataLocation::CallData))
{
solAssert(!_fromMemory, "");
Copy link
Contributor

Choose a reason for hiding this comment

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

Can it happen that dataStoredIn(DataLocation::Memory) is true, but _fromMemory is false?

Copy link
Member Author

Choose a reason for hiding this comment

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

I think so - if we want to decode something in calldata into memory, I think.

m_context << Instruction::DUP1;
m_context << typeOnStack.calldataEncodedSize(true);
m_context << Instruction::ADD;
abiDecode({targetType.shared_from_this()}, false);
Copy link
Contributor

Choose a reason for hiding this comment

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

Please check that this properly allocates memory.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, but this might need calldata size checks, at least for dynamically-sized structs.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yep - adjusted this (and the same for the other nested abiDecode call) in a new commit - should be cheaper with CALLDATASIZE and SUB anyways.

{
solUnimplementedAssert(!type.isDynamicallyEncoded(), "");
m_context << type.calldataOffsetOfMember(member) << Instruction::ADD;
if (_memberAccess.annotation().type->isValueType())
Copy link
Contributor

Choose a reason for hiding this comment

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

Does this work for arrays?

Copy link
Contributor

Choose a reason for hiding this comment

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

Please also add explicit assertions for every non-value type this is designed to work with.

Copy link
Member Author

Choose a reason for hiding this comment

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

For every non-value type it should be fine to just return the calldata offset - I think the TypeChecker shouldn't allow anything that will actually try to further decode, if this is an array and if so the the actual decoding of the calldata array should have the unimplemented assertion.

Copy link
Contributor

Choose a reason for hiding this comment

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

Still better to assert all types we know are working, so that it automatically breaks if we introduce new types.

m_context << Instruction::DUP1;
m_context << type.calldataEncodedSize(true);
m_context << Instruction::ADD;
CompilerUtils(m_context).abiDecode({_memberAccess.annotation().type}, false);
Copy link
Contributor

Choose a reason for hiding this comment

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

For uint256, this will turn into an assembly function call where the function only consists of a single calldataload, which cannot be inlined by the current optimizer. This is quite inefficient. Should we make a small exception for types where storageBytes is 32 and call loadFromMemoryDynamic as above? Actually loadFromMemoryDynamic also performs range checks. So another solution would be to update CompilerUtils::convertType( together with #5815

Copy link
Member Author

Choose a reason for hiding this comment

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

storageBytes is probably right, but feels a bit off... calldataEncodedSize(false) should do it as well, shouldn't it? And I'm wondering whether this actually needs to range-check that we're inside calldatasize or whether we can just use calldataload directly if calldataEncodedSize(false)==32... probably enough for the statically encoded case, right?

Copy link
Contributor

Choose a reason for hiding this comment

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

We do not need any calldatasize-range checks here for any value type. Using calldataEncodedSize is better, yes. And you can just use calldataload, yes :)

Copy link
Member Author

Choose a reason for hiding this comment

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

Or... we could just always call CompilerUtils(m_context).loadFromMemoryDynamic(*_memberAccess.annotation().type, true, false, false) here, that should do it and degenerate to a single calldataload whenever possible - I guess that's what you meant with updating CompilerUtils::convertType( together with #5815, so that (at least in this case) loadFromMemoryDynamic will validate instead of clean?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, exactly.

@chriseth
Copy link
Contributor

chriseth commented Feb 7, 2019

Are calldata size checks properly enforced here? I think you already answered that question when I meant something else ;)

@ekpyron
Copy link
Member Author

ekpyron commented Feb 7, 2019

@chriseth Actually I thought they were, but now I'm not sure anymore, I'll recheck and make sure!

@chriseth
Copy link
Contributor

chriseth commented Feb 7, 2019

If we limit the scope of this PR to statically encoded structs, the only things left to do in my opinion are:

  1. try to get uint member access a bit more efficient.
  2. ssert that member access for non-value types is either a struct or an array.

if (_memberAccess.annotation().type->isValueType())
{
solAssert(_memberAccess.annotation().type->calldataEncodedSize(false) > 0, "");
CompilerUtils(m_context).loadFromMemoryDynamic(*_memberAccess.annotation().type, true, false, false);
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
CompilerUtils(m_context).loadFromMemoryDynamic(*_memberAccess.annotation().type, true, false, false);
CompilerUtils(m_context).loadFromMemoryDynamic(*_memberAccess.annotation().type, true, true, false);

(struct members are always padded)

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't think so - this will end up in CompilerUtils.cpp#L1261 and will make sure everything with calldataEncodedSize(false) != 32 will be cleaned (or at some point validated I guess).

Copy link
Member Author

Choose a reason for hiding this comment

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

I was in fact wrong, it will trigger the shifting in loadFromMemoryHelper - which we may in fact not want here. I'll check and recheck with some more tests, but I think you're right in that it should be true here.

CompilerUtils(m_context).loadFromMemoryDynamic(*_memberAccess.annotation().type, true, false, false);
}
else
solUnimplementedAssert(
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
solUnimplementedAssert(
solAssert(

Are you planning on implemented anything else apart from arrays and structs? :)

Copy link
Member Author

Choose a reason for hiding this comment

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

This can only happen for something that doesn't exist yet anyways and if we add something that could fail here, then handling that is unimplemented, right :-)? At least that was the idea - I can change to an assert, though, I don't mind either :-).

Copy link
Contributor

@chriseth chriseth left a comment

Choose a reason for hiding this comment

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

Needs tests for calldata structs with short calldata and dirty higher order bits.

// double check that the valid case goes through
ABI_CHECK(callContractFunction("f((uint256,uint256))", u256(1), u256(2)), encodeArgs(0x44));

ABI_CHECK(callContractFunctionNoEncoding("f((uint256,uint256))", bytes(63,0)), encodeArgs());
Copy link
Member Author

Choose a reason for hiding this comment

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

@chriseth Sorry, I force pushed your comment away - but this is missing one byte, that's why it should revert.

@chriseth
Copy link
Contributor

chriseth commented Feb 7, 2019

Looks good, please squash!

@ekpyron
Copy link
Member Author

ekpyron commented Feb 7, 2019

Still needs more tests, I think.

@chriseth
Copy link
Contributor

chriseth commented Feb 7, 2019

Hm, perhaps a struct containing an external function pointer - those are always a little nasty.

@ekpyron
Copy link
Member Author

ekpyron commented Feb 7, 2019

Also abi.decode lacks tests (for calldata structs) so far, resp. doesn't support calldata structs, but should.

@ekpyron ekpyron changed the title [WIP] ABIEncoderV2: Implement calldata structs without dynamically encoded members. ABIEncoderV2: Implement calldata structs without dynamically encoded members. Feb 11, 2019
@ekpyron
Copy link
Member Author

ekpyron commented Feb 11, 2019

Added another test case for function pointers. I think it makes more sense to keep abi.decode as is for now (i.e. always copy to memory) and change it, when we also have calldata arrays, internal calladata calls and local calldata variables.

@ekpyron ekpyron dismissed chriseth’s stale review February 11, 2019 13:48

Everything should be addressed.

@ekpyron ekpyron changed the title ABIEncoderV2: Implement calldata structs without dynamically encoded members. [WIP] ABIEncoderV2: Implement calldata structs without dynamically encoded members. Feb 11, 2019
@ekpyron
Copy link
Member Author

ekpyron commented Feb 11, 2019

Do not merge yet - I may have missed one case in the TypeChecker - just verifying. Nevermind, should be fine.

@ekpyron ekpyron changed the title [WIP] ABIEncoderV2: Implement calldata structs without dynamically encoded members. ABIEncoderV2: Implement calldata structs without dynamically encoded members. Feb 11, 2019
@chriseth chriseth merged commit 92cb6cb into develop Feb 11, 2019
@ekpyron ekpyron deleted the calldataStructsV2 branch February 11, 2019 15:19
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