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

VM: Strip zeros when putting contract storage in StateManager #880

Merged
merged 4 commits into from
Oct 7, 2020
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions packages/vm/lib/state/stateManager.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const Set = require('core-js-pure/es/set')
import { SecureTrie as Trie } from 'merkle-patricia-tree'
import { BN, toBuffer, keccak256, KECCAK256_NULL } from 'ethereumjs-util'
import { BN, toBuffer, keccak256, KECCAK256_NULL, unpadBuffer } from 'ethereumjs-util'
import { encode, decode } from 'rlp'
import Common from '@ethereumjs/common'
import { genesisStateByName } from '@ethereumjs/common/dist/genesisStates'
Expand Down Expand Up @@ -276,13 +276,19 @@ export default class DefaultStateManager implements StateManager {
* corresponding to `address` at the provided `key`.
* @param address - Address to set a storage value for
* @param key - Key to set the value at. Must be 32 bytes long.
* @param value - Value to set at `key` for account corresponding to `address`
* @param value - Value to set at `key` for account corresponding to `address`. Cannot be more than 32 bytes. Leading zeros are stripped. If it is a empty or filled with zeros, deletes the value.
*/
async putContractStorage(address: Buffer, key: Buffer, value: Buffer): Promise<void> {
if (key.length !== 32) {
throw new Error('Storage key must be 32 bytes long')
}

if (value.length > 32) {
throw new Error('Storage key cannot be longer than 32 bytes')
jochem-brouwer marked this conversation as resolved.
Show resolved Hide resolved
}

value = unpadBuffer(value)
Copy link
Member

Choose a reason for hiding this comment

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

You are writing in the comments that a value will be left-padded and then you actually unpad (I read the semantics of this function as: "remove the padding", correct me if I am wrong) here, how does this go together?

Copy link
Member Author

Choose a reason for hiding this comment

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

Oops this is a comment from when I was still thinking that storage slots should always be 32 bytes; this is thus not the case, the leading zeros are stripped. Will update


await this._modifyContractStorage(address, async (storageTrie, done) => {
if (value && value.length) {
// format input
Expand Down Expand Up @@ -415,7 +421,7 @@ export default class DefaultStateManager implements StateManager {
}

/**
* Dumps the the storage values for an `account` specified by `address`.
* Dumps the RLP-encoded storage values for an `account` specified by `address`.
* @param address - The address of the `account` to return storage for
* @returns {Promise<StorageDump>} - The state of the account as an `Object` map.
* Keys are are the storage keys, values are the storage values as strings.
Expand Down
79 changes: 78 additions & 1 deletion packages/vm/tests/api/state/stateManager.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as tape from 'tape'
import { BN, toBuffer, keccak256, KECCAK256_RLP } from 'ethereumjs-util'
import { BN, toBuffer, keccak256, KECCAK256_RLP, unpadBuffer, zeros } from 'ethereumjs-util'
import Common from '@ethereumjs/common'
import Account from '@ethereumjs/account'
import { DefaultStateManager } from '../../../lib/state'
Expand Down Expand Up @@ -409,3 +409,80 @@ tape('StateManager - Contract code', (tester) => {
t.end()
})
})

tape('StateManager - Contract storage', (tester) => {
const it = tester.test

it('should throw on storage values larger than 32 bytes', async (t) => {
t.plan(1)
const stateManager = new DefaultStateManager()
const address = zeros(20)
const key = zeros(32)
const value = Buffer.from('aa'.repeat(33), 'hex')
try {
await stateManager.putContractStorage(address, key, value)
t.fail('did not throw')
} catch (e) {
t.pass('threw on trying to set storage values larger than 32 bytes')
}
t.end()
})

it('should strip storage values lower than 32 bytes', async (t) => {
Copy link
Member

Choose a reason for hiding this comment

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

This test description is a bit misleading (stripping is logically not connected to the byte length) respectively also not correct (stripping is also done for 32-byte length. Can you please adjust?

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 have edited this name in 5217f50, hope this is better. I think this name was from when I still expected storage values to always be 32 bytes and I did not edit it right when I realized that was wrong.

const stateManager = new DefaultStateManager()
const address = zeros(20)
const key0 = zeros(32)
const value0 = Buffer.from('aa'.repeat(31), 'hex') // put a value of 31-bytes length
const expect0 = unpadBuffer(value0)
const key1 = Buffer.concat([zeros(31), Buffer.from('01', 'hex')])
const value1 = Buffer.from('aa'.repeat(1), 'hex') // put a vlaue of 1-byte length
jochem-brouwer marked this conversation as resolved.
Show resolved Hide resolved
const expect1 = unpadBuffer(value1)

await stateManager.putContractStorage(address, key0, value0)
const slot0 = await stateManager.getContractStorage(address, key0)
t.ok(slot0.equals(expect0), 'value of 31 bytes padded correctly')
Copy link
Member

Choose a reason for hiding this comment

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

Really nit, but I would think this test would be a bit better readable if the respective execution part of these two cases are combined with the value setup (so everything with *0 together and everything with *1 together)

Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed -> 5217f50

Note: I have edited this test a little bit (it did unpadZeros in previous commits on this particular test, but none of the test cases actually had leading zeros)


await stateManager.putContractStorage(address, key1, value1)
const slot1 = await stateManager.getContractStorage(address, key1)
t.ok(slot1.equals(expect1), 'value of 1 byte padded correctly')
t.end()
})

it('should delete storage values which only consist of zero bytes', async (t) => {
const address = zeros(20)
const key = zeros(32)
const startValue = Buffer.from('01', 'hex')

const zeroLengths = [0, 1, 31, 32] // checks for arbitrary-length zeros
t.plan(zeroLengths.length)

for (let length of zeroLengths) {
const stateManager = new DefaultStateManager()
const value = zeros(length)
await stateManager.putContractStorage(address, key, startValue)
const currentValue = await stateManager.getContractStorage(address, key)
if (!currentValue.equals(startValue)) {
// sanity check
t.fail('contract value not set correctly')
} else {
// delete the value
await stateManager.putContractStorage(address, key, value)
const deleted = await stateManager.getContractStorage(address, key)
t.ok(deleted.equals(zeros(0)), 'the storage key should be deleted')
}
}
t.end()
})

it('should not strip trailing zeros', async (t) => {
const address = zeros(20)
const key = zeros(32)
const value = Buffer.from('0000aabb00', 'hex')
const expect = Buffer.from('aabb00', 'hex')
const stateManager = new DefaultStateManager()
await stateManager.putContractStorage(address, key, value)
const contractValue = await stateManager.getContractStorage(address, key)
t.ok(contractValue.equals(expect), 'trailing zeros are not stripped')
t.end()
})
})