-
Notifications
You must be signed in to change notification settings - Fork 750
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
Conversation
Guard mem read against zero length Fix saving call return data to memory Minor improvements Convert offset and length to number before mem write
Will directly take a look here once after we settled out the Blockchain stuff (probably we more or less have). |
We have |
Since this is now so beautifully isolated, can you add some few simple tests for Should be ok to add these to the |
Both good points, updated. |
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 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) | ||
} | ||
} |
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.
Ok, equivalent to the old memLoad
function, subMemUsage
and zero check extracted to opcodes.
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.
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()) | ||
} |
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.
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) |
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.
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) { |
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.
My intuition here would be to have the parameter sorting the other way around, so function getDataSlice (data, offset, 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.
Yeah, that makes more sense, updated.
memOffset = memOffset.toNumber() | ||
dataLength = dataLength.toNumber() | ||
runState.memory.extend(memOffset, dataLength) | ||
runState.memory.write(memOffset, dataLength, data) |
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.
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()) |
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 are these zero checks not necessary here?
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 question, because no test failed for them 😅
Fixed all reads that had no length check, thanks for catching!
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.
😄
let data = Buffer.alloc(0) | ||
if (!inLength.isZero()) { | ||
data = runState.memory.read(inOffset.toNumber(), inLength.toNumber()) | ||
} |
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.
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()) |
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.
Same questions here.
runState.returnValue = Buffer.alloc(0) | ||
if (!length.isZero()) { | ||
runState.returnValue = runState.memory.read(offset.toNumber(), length.toNumber()) | ||
} |
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.
Both ok.
const dataLength = localOpts.outLength.toNumber() | ||
runState.memory.extend(memOffset, dataLength) | ||
runState.memory.write(memOffset, dataLength, data) | ||
} |
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.
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.
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() | ||
}) | ||
}) |
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.
Cool, thanks! 😄
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.
Looks good.
@@ -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()) { |
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 would it be a problem allowing reading 0 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.
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.
This PR:
Memory
class used for evm memory manipulationmemStore
andmemLoad
and moves the logic to opcode handlers. The goal is to increase verbosity and reduce code hidden in utility functionsExtCodeCopyTargetRangeLongerThanCodeTests
andcodeCopyZero
which were skipped previously have been fixed and are no longer skippedSimilar to before,
offset
andlength
are cast fromBN
to normal JS numbers, which means if they're bigger than 53 bitsBN
throws an exception. Gas limit should probably cap how bugoffset
andlength
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 aMap
instead of a normal array for memory, and to store indices as aBuffer
which can be arbitrary long. But I imagine this would be less performant.