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

Refactor memory manipulation in evm #442

Merged
merged 6 commits into from
Feb 21, 2019
Merged

Refactor memory manipulation in evm #442

merged 6 commits into from
Feb 21, 2019

Conversation

s1na
Copy link
Contributor

@s1na s1na commented Feb 20, 2019

This PR:

  • Introduces the Memory class used for evm memory manipulation
  • Removes memStore and memLoad and moves the logic to opcode handlers. The goal is to increase verbosity and reduce code hidden in utility functions
  • In some opcodes, it reduces copy gas before attempting memory write
  • The test cases ExtCodeCopyTargetRangeLongerThanCodeTests and codeCopyZero which were skipped previously have been fixed and are no longer skipped
  • This is part of the same effort as in Refactor evm and prepare for ewasm integration #424 and Basic support for ewasm precompiles #431.

Similar to before, offset and length are cast from BN to normal JS numbers, which means if they're bigger than 53 bits BN throws an exception. Gas limit should probably cap how bug offset and length can become, but it might be better to address this issue somehow in future. The only solution that came to my mind is to use a Map instead of a normal array for memory, and to store indices as a Buffer which can be arbitrary long. But I imagine this would be less performant.

Guard mem read against zero length

Fix saving call return data to memory

Minor improvements

Convert offset and length to number before mem write
@holgerd77
Copy link
Member

Will directly take a look here once after we settled out the Blockchain stuff (probably we more or less have).

@holgerd77
Copy link
Member

We have memory exposed in the the step event. This should probably be changed to runState.memory._store (or some getter) in runStepHook (this is equivalent to the old runState.memory, do I get this right?) to not introduce some breaking change here.

@holgerd77
Copy link
Member

Since this is now so beautifully isolated, can you add some few simple tests for extend, write and read? Think this would be worth it, also to have some more explicit usage examples where devs can more easily see how memory works.

Should be ok to add these to the API suite though strictly speaking no (public) API.

@s1na
Copy link
Contributor Author

s1na commented Feb 21, 2019

Both good points, updated.

@coveralls
Copy link

coveralls commented Feb 21, 2019

Coverage Status

Coverage increased (+0.1%) to 89.272% when pulling 14a3505 on refactor/vm-mem into b03957c on master.

Copy link
Member

@holgerd77 holgerd77 left a comment

Choose a reason for hiding this comment

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

I could only grab after the review how much of an improvement this actually is, thanks Sina for this, great! 😄 👍

Some smaller questions.

}
return Buffer.from(loaded)
}
}
Copy link
Member

Choose a reason for hiding this comment

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

Ok, equivalent to the old memLoad function, subMemUsage and zero check extracted to opcodes.

Copy link
Member

Choose a reason for hiding this comment

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

Second note: signature changed from Buffer to Number for offset and size, but better to have this explicit here than having this inner conversion, good.

let data = Buffer.alloc(0)
if (!length.isZero()) {
data = runState.memory.read(offset.toNumber(), length.toNumber())
}
Copy link
Member

Choose a reason for hiding this comment

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

Ok.

@@ -252,17 +256,27 @@ module.exports = {
}
},
CALLDATACOPY: function (memOffset, dataOffset, dataLength, runState) {
memStore(runState, memOffset, runState.callData, dataOffset, dataLength)
// sub the COPY fee
subMemUsage(runState, memOffset, dataLength)
Copy link
Member

Choose a reason for hiding this comment

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

Had a really hard time to understand why this is now included until I understood that the following code from the old memStore is actually evaluating to false by skipSubMem being undefined when not passed to the function:

function memStore (runState, offset, val, valOffset, length, skipSubMem) {
  if (skipSubMem !== false) {
    subMemUsage(runState, offset, length)
  }
  //...
}

So so glad that we get this out here. Phew. 😄

lib/vm/opFns.js Outdated
function memStore (runState, offset, val, valOffset, length, skipSubMem) {
if (skipSubMem !== false) {
subMemUsage(runState, offset, length)
function getDataSlice (offset, length, data) {
Copy link
Member

Choose a reason for hiding this comment

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

My intuition here would be to have the parameter sorting the other way around, so function getDataSlice (data, offset, length).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, that makes more sense, updated.

memOffset = memOffset.toNumber()
dataLength = dataLength.toNumber()
runState.memory.extend(memOffset, dataLength)
runState.memory.write(memOffset, dataLength, data)
Copy link
Member

Choose a reason for hiding this comment

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

Just some exemplary analysis for my own understanding: the old memStore functionality is separated into getDataSlice (taking the valOffset from the memStore signature) and then the extend and write functionality.

This makes things much more explicit by breaking up the old memStore functionality which is actually currently to a greater extend unreadable and pretty hard to decipher.

👍

lib/vm/opFns.js Outdated
@@ -576,7 +617,8 @@ module.exports = {
trap(ERROR.STATIC_STATE_CHANGE)
}

var data = memLoad(runState, inOffset, inLength)
subMemUsage(runState, inOffset, inLength)
const data = runState.memory.read(inOffset.toNumber(), inLength.toNumber())
Copy link
Member

Choose a reason for hiding this comment

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

Why are these zero checks not necessary 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.

Good question, because no test failed for them 😅

Fixed all reads that had no length check, thanks for catching!

Copy link
Member

Choose a reason for hiding this comment

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

😄

let data = Buffer.alloc(0)
if (!inLength.isZero()) {
data = runState.memory.read(inOffset.toNumber(), inLength.toNumber())
}
Copy link
Member

Choose a reason for hiding this comment

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

Ok.

lib/vm/opFns.js Outdated
@@ -728,7 +775,8 @@ module.exports = {
var value = new BN(0)
toAddress = addressToBuffer(toAddress)

var data = memLoad(runState, inOffset, inLength)
subMemUsage(runState, inOffset, inLength)
const data = runState.memory.read(inOffset.toNumber(), inLength.toNumber())
Copy link
Member

Choose a reason for hiding this comment

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

Same questions here.

runState.returnValue = Buffer.alloc(0)
if (!length.isZero()) {
runState.returnValue = runState.memory.read(offset.toNumber(), length.toNumber())
}
Copy link
Member

Choose a reason for hiding this comment

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

Both ok.

const dataLength = localOpts.outLength.toNumber()
runState.memory.extend(memOffset, dataLength)
runState.memory.write(memOffset, dataLength, data)
}
Copy link
Member

Choose a reason for hiding this comment

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

Good.

Copy link
Member

@holgerd77 holgerd77 left a comment

Choose a reason for hiding this comment

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

So this generally looks really good, have gone through all the code. Since this is also highly covered by the consensus test suite it should be ok to merge once last things are addressed.

st.throws(() => m.write(0, 5, Buffer.from([8, 8, 8])), /size/)
st.end()
})
})
Copy link
Member

Choose a reason for hiding this comment

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

Cool, thanks! 😄

Copy link
Member

@holgerd77 holgerd77 left a comment

Choose a reason for hiding this comment

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

Looks good.

@holgerd77 holgerd77 merged commit d0ad8d1 into master Feb 21, 2019
@holgerd77 holgerd77 deleted the refactor/vm-mem branch February 21, 2019 13:28
@@ -587,7 +587,10 @@ module.exports = {
}

subMemUsage(runState, offset, length)
const data = runState.memory.read(offset.toNumber(), length.toNumber())
let data = Buffer.alloc(0)
if (!length.isZero()) {
Copy link
Member

Choose a reason for hiding this comment

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

Why would it be a problem allowing reading 0 length?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Checking for 0 length was being done in memLoad. I tried omitting it, but faced a problem: tests with an offset wider than 53 bits would fail (BN.toNumber() would throw). In these test cases the length is 0 (as even length 1 would need a lot of gas I think). So I found this check is basically a way to make the tests pass! I'm thinking about how we can improve on this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants