[PAN-2829] Account versioning (#1612)

Initial EIP-1702 versioning code.  Version internally is an `int` for Java 
reasons. In the future if the versions become sparse or large we will create a 
"proxy" version index by putting the standardized indexes into an enum and 
using the enum ordinal instead.

Reference tests all pass (hence the WorldStateMock if block) and
GenesisStateTest has an explicit version test now.

Signed-off-by: Adrian Sutton <adrian.sutton@consensys.net>
pull/2/head
Danno Ferrin 5 years ago committed by GitHub
parent 8ca333f2fe
commit 9a9b7e9dd6
  1. 4
      config/src/main/java/tech/pegasys/pantheon/config/GenesisAllocation.java
  2. 17
      ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/chain/GenesisState.java
  3. 16
      ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/core/AbstractWorldUpdater.java
  4. 11
      ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/core/Account.java
  5. 7
      ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/core/MutableAccount.java
  6. 27
      ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/mainnet/MainnetContractCreationProcessor.java
  7. 16
      ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/mainnet/MainnetProtocolSpecs.java
  8. 21
      ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/worldstate/DefaultMutableWorldState.java
  9. 38
      ethereum/core/src/main/java/tech/pegasys/pantheon/ethereum/worldstate/StateTrieAccountValue.java
  10. 1
      ethereum/core/src/test-support/java/tech/pegasys/pantheon/ethereum/core/BlockDataGenerator.java
  11. 2
      ethereum/core/src/test-support/java/tech/pegasys/pantheon/ethereum/core/MessageFrameTestFixture.java
  12. 24
      ethereum/core/src/test/java/tech/pegasys/pantheon/ethereum/chain/GenesisStateTest.java
  13. 30
      ethereum/core/src/test/java/tech/pegasys/pantheon/ethereum/vm/WorldStateMock.java
  14. 10
      ethereum/core/src/test/java/tech/pegasys/pantheon/ethereum/vm/operations/ExtCodeHashOperationTest.java
  15. 3
      ethereum/core/src/test/java/tech/pegasys/pantheon/ethereum/worldstate/DefaultMutableWorldStateTest.java
  16. 834
      ethereum/core/src/test/resources/tech/pegasys/pantheon/ethereum/chain/genesis4.json

@ -37,6 +37,10 @@ public class GenesisAllocation {
return data.getString("code");
}
public String getVersion() {
return data.getString("version");
}
public Map<String, Object> getStorage() {
return data.getJsonObject("storage", new JsonObject()).getMap();
}

@ -14,6 +14,7 @@ package tech.pegasys.pantheon.ethereum.chain;
import tech.pegasys.pantheon.config.GenesisAllocation;
import tech.pegasys.pantheon.config.GenesisConfigFile;
import tech.pegasys.pantheon.ethereum.core.Account;
import tech.pegasys.pantheon.ethereum.core.Address;
import tech.pegasys.pantheon.ethereum.core.Block;
import tech.pegasys.pantheon.ethereum.core.BlockBody;
@ -111,6 +112,7 @@ public final class GenesisState {
final MutableAccount account = updater.getOrCreate(genesisAccount.address);
account.setBalance(genesisAccount.balance);
account.setCode(genesisAccount.code);
account.setVersion(genesisAccount.version);
genesisAccount.storage.forEach(account::setStorageValue);
});
updater.commit();
@ -216,25 +218,29 @@ public final class GenesisState {
final Address address;
final Wei balance;
final BytesValue code;
final Map<UInt256, UInt256> storage;
final BytesValue code;
final int version;
public static GenesisAccount fromAllocation(final GenesisAllocation allocation) {
static GenesisAccount fromAllocation(final GenesisAllocation allocation) {
return new GenesisAccount(
allocation.getAddress(),
allocation.getBalance(),
allocation.getStorage(),
allocation.getCode(),
allocation.getStorage());
allocation.getVersion());
}
private GenesisAccount(
final String hexAddress,
final String balance,
final Map<String, Object> storage,
final String hexCode,
final Map<String, Object> storage) {
final String version) {
this.address = withNiceErrorMessage("address", hexAddress, Address::fromHexString);
this.balance = withNiceErrorMessage("balance", balance, this::parseBalance);
this.code = hexCode != null ? BytesValue.fromHexString(hexCode) : null;
this.version = version != null ? Integer.decode(version) : Account.DEFAULT_VERSION;
this.storage = parseStorage(storage);
}
@ -265,8 +271,9 @@ public final class GenesisState {
return MoreObjects.toStringHelper(this)
.add("address", address)
.add("balance", balance)
.add("code", code)
.add("storage", storage)
.add("code", code)
.add("version", version)
.toString();
}
}

@ -163,6 +163,7 @@ public abstract class AbstractWorldUpdater<W extends WorldView, A extends Accoun
private long nonce;
private Wei balance;
private int version;
@Nullable private BytesValue updatedCode; // Null if the underlying code has not been updated.
@Nullable private Hash updatedCodeHash;
@ -179,6 +180,7 @@ public abstract class AbstractWorldUpdater<W extends WorldView, A extends Accoun
this.nonce = 0;
this.balance = Wei.ZERO;
this.version = Account.DEFAULT_VERSION;
this.updatedCode = BytesValue.EMPTY;
this.updatedStorage = new TreeMap<>();
@ -192,6 +194,7 @@ public abstract class AbstractWorldUpdater<W extends WorldView, A extends Accoun
this.nonce = account.getNonce();
this.balance = account.getBalance();
this.version = account.getVersion();
this.updatedStorage = new TreeMap<>();
}
@ -283,6 +286,16 @@ public abstract class AbstractWorldUpdater<W extends WorldView, A extends Accoun
this.updatedCode = code;
}
@Override
public void setVersion(final int version) {
this.version = version;
}
@Override
public int getVersion() {
return version;
}
@Override
public UInt256 getStorageValue(final UInt256 key) {
final UInt256 value = updatedStorage.get(key);
@ -358,7 +371,7 @@ public abstract class AbstractWorldUpdater<W extends WorldView, A extends Accoun
static class StackedUpdater<W extends WorldView, A extends Account>
extends AbstractWorldUpdater<AbstractWorldUpdater<W, A>, UpdateTrackingAccount<A>> {
protected StackedUpdater(final AbstractWorldUpdater<W, A> world) {
StackedUpdater(final AbstractWorldUpdater<W, A> world) {
super(world);
}
@ -419,6 +432,7 @@ public abstract class AbstractWorldUpdater<W extends WorldView, A extends Accoun
existing.setBalance(update.getBalance());
if (update.codeWasUpdated()) {
existing.setCode(update.getCode());
existing.setVersion(update.getVersion());
}
if (update.getStorageWasCleared()) {
existing.clearStorage();

@ -31,12 +31,14 @@ import java.util.NavigableMap;
* practice, only non-zero mappings are stored and setting a key to the value 0 is akin to
* "removing" that key).
* <li><b>Code:</b> arbitrary-length sequence of bytes that corresponds to EVM bytecode.
* <li><b>Version:</b> the version of the EVM bytecode.
* </ul>
*/
public interface Account {
long DEFAULT_NONCE = 0L;
Wei DEFAULT_BALANCE = Wei.ZERO;
int DEFAULT_VERSION = 0;
/**
* The Keccak-256 hash of the account address.
@ -83,7 +85,7 @@ public interface Account {
/**
* The hash of the EVM bytecode associated with this account.
*
* @return the hash of the account code (which may be {@link Hash#EMPTY}.
* @return the hash of the account code (which may be {@link Hash#EMPTY}).
*/
Hash getCodeHash();
@ -99,6 +101,13 @@ public interface Account {
return !getCode().isEmpty();
}
/**
* The version of the EVM bytecode associated with this account.
*
* @return the version of the account code. Default is zero.
*/
int getVersion();
/**
* Retrieves a value in the account storage given its key.
*

@ -82,6 +82,13 @@ public interface MutableAccount extends Account {
*/
void setCode(BytesValue code);
/**
* Sets the version for the account.
*
* @param version the version of the code being set
*/
void setVersion(int version);
/**
* Sets a particular key-value pair in the account storage.
*

@ -40,18 +40,39 @@ public class MainnetContractCreationProcessor extends AbstractMessageProcessor {
private final int codeSizeLimit;
private final int accountVersion;
public MainnetContractCreationProcessor(
final GasCalculator gasCalculator,
final EVM evm,
final boolean requireCodeDepositToSucceed,
final int codeSizeLimit,
final long initialContractNonce,
final Collection<Address> forceCommitAddresses) {
final Collection<Address> forceCommitAddresses,
final int accountVersion) {
super(evm, forceCommitAddresses);
this.gasCalculator = gasCalculator;
this.requireCodeDepositToSucceed = requireCodeDepositToSucceed;
this.codeSizeLimit = codeSizeLimit;
this.initialContractNonce = initialContractNonce;
this.accountVersion = accountVersion;
}
public MainnetContractCreationProcessor(
final GasCalculator gasCalculator,
final EVM evm,
final boolean requireCodeDepositToSucceed,
final int codeSizeLimit,
final long initialContractNonce,
final Collection<Address> forceCommitAddresses) {
this(
gasCalculator,
evm,
requireCodeDepositToSucceed,
codeSizeLimit,
initialContractNonce,
forceCommitAddresses,
Account.DEFAULT_VERSION);
}
public MainnetContractCreationProcessor(
@ -66,7 +87,8 @@ public class MainnetContractCreationProcessor extends AbstractMessageProcessor {
requireCodeDepositToSucceed,
codeSizeLimit,
initialContractNonce,
ImmutableSet.of());
ImmutableSet.of(),
Account.DEFAULT_VERSION);
}
private static boolean accountExists(final Account account) {
@ -131,6 +153,7 @@ public class MainnetContractCreationProcessor extends AbstractMessageProcessor {
final MutableAccount contract =
frame.getWorldState().getOrCreate(frame.getContractAddress());
contract.setCode(contractCode);
contract.setVersion(accountVersion);
LOG.trace(
"Successful creation of contract {} with code of size {} (Gas remaining: {})",
frame.getContractAddress(),

@ -265,11 +265,23 @@ public abstract class MainnetProtocolSpecs {
public static ProtocolSpecBuilder<Void> istanbulDefinition(
final Optional<BigInteger> chainId,
final OptionalInt contractSizeLimit,
final OptionalInt configContractSizeLimit,
final OptionalInt configStackSizeLimit,
final boolean enableRevertReason) {
final int contractSizeLimit =
configContractSizeLimit.orElse(SPURIOUS_DRAGON_CONTRACT_SIZE_LIMIT);
return constantinopleFixDefinition(
chainId, contractSizeLimit, configStackSizeLimit, enableRevertReason)
chainId, configContractSizeLimit, configStackSizeLimit, enableRevertReason)
.contractCreationProcessorBuilder(
(gasCalculator, evm) ->
new MainnetContractCreationProcessor(
gasCalculator,
evm,
true,
contractSizeLimit,
1,
SPURIOUS_DRAGON_FORCE_DELETE_WHEN_EMPTY_ADDRESSES,
1))
.name("Istanbul");
}

@ -108,9 +108,13 @@ public class DefaultMutableWorldState implements MutableWorldState {
}
private static BytesValue serializeAccount(
final long nonce, final Wei balance, final Hash storageRoot, final Hash codeHash) {
final long nonce,
final Wei balance,
final Hash storageRoot,
final Hash codeHash,
final int version) {
final StateTrieAccountValue accountValue =
new StateTrieAccountValue(nonce, balance, storageRoot, codeHash);
new StateTrieAccountValue(nonce, balance, storageRoot, codeHash, version);
return RLP.encode(accountValue::writeTo);
}
@ -243,6 +247,11 @@ public class DefaultMutableWorldState implements MutableWorldState {
return accountValue.getCodeHash();
}
@Override
public int getVersion() {
return accountValue.getVersion();
}
@Override
public UInt256 getStorageValue(final UInt256 key) {
final Optional<BytesValue> val = storageTrie().get(Hash.hash(key.getBytes()));
@ -283,6 +292,7 @@ public class DefaultMutableWorldState implements MutableWorldState {
builder.append("balance=").append(getBalance()).append(", ");
builder.append("storageRoot=").append(getStorageRoot()).append(", ");
builder.append("codeHash=").append(getCodeHash());
builder.append("version=").append(getVersion());
return builder.append("}").toString();
}
}
@ -364,7 +374,12 @@ public class DefaultMutableWorldState implements MutableWorldState {
// Lastly, save the new account.
final BytesValue account =
serializeAccount(updated.getNonce(), updated.getBalance(), storageRoot, codeHash);
serializeAccount(
updated.getNonce(),
updated.getBalance(),
storageRoot,
codeHash,
updated.getVersion());
wrapped.accountStateTrie.put(updated.getAddressHash(), account);
}

@ -12,6 +12,7 @@
*/
package tech.pegasys.pantheon.ethereum.worldstate;
import tech.pegasys.pantheon.ethereum.core.Account;
import tech.pegasys.pantheon.ethereum.core.Hash;
import tech.pegasys.pantheon.ethereum.core.Wei;
import tech.pegasys.pantheon.ethereum.rlp.RLPInput;
@ -24,13 +25,24 @@ public class StateTrieAccountValue {
private final Wei balance;
private final Hash storageRoot;
private final Hash codeHash;
private final int version;
public StateTrieAccountValue(
private StateTrieAccountValue(
final long nonce, final Wei balance, final Hash storageRoot, final Hash codeHash) {
this(nonce, balance, storageRoot, codeHash, Account.DEFAULT_VERSION);
}
public StateTrieAccountValue(
final long nonce,
final Wei balance,
final Hash storageRoot,
final Hash codeHash,
final int version) {
this.nonce = nonce;
this.balance = balance;
this.storageRoot = storageRoot;
this.codeHash = codeHash;
this.version = version;
}
/**
@ -63,12 +75,21 @@ public class StateTrieAccountValue {
/**
* The hash of the EVM bytecode associated with this account.
*
* @return the hash of the account code (which may be {@link Hash#EMPTY}.
* @return the hash of the account code (which may be {@link Hash#EMPTY}).
*/
public Hash getCodeHash() {
return codeHash;
}
/**
* The version of the EVM bytecode associated with this account.
*
* @return the version of the account code.
*/
public int getVersion() {
return version;
}
public void writeTo(final RLPOutput out) {
out.startList();
@ -77,6 +98,11 @@ public class StateTrieAccountValue {
out.writeBytesValue(storageRoot);
out.writeBytesValue(codeHash);
if (version != Account.DEFAULT_VERSION) {
// version of zero is never written out.
out.writeLongScalar(version);
}
out.endList();
}
@ -87,9 +113,15 @@ public class StateTrieAccountValue {
final Wei balance = in.readUInt256Scalar(Wei::wrap);
final Hash storageRoot = Hash.wrap(in.readBytes32());
final Hash codeHash = Hash.wrap(in.readBytes32());
final int version;
if (!in.isEndOfCurrentList()) {
version = in.readIntScalar();
} else {
version = Account.DEFAULT_VERSION;
}
in.leaveList();
return new StateTrieAccountValue(nonce, balance, storageRoot, codeHash);
return new StateTrieAccountValue(nonce, balance, storageRoot, codeHash, version);
}
}

@ -153,6 +153,7 @@ public class BlockDataGenerator {
if (random.nextFloat() < percentContractAccounts) {
// Some percentage of accounts are contract accounts
account.setCode(bytesValue(5, 50));
account.setVersion(Account.DEFAULT_VERSION);
if (random.nextFloat() < percentContractAccountsWithNonEmptyStorage) {
// Add some storage for contract accounts
int storageValues = random.nextInt(20) + 10;

@ -30,7 +30,7 @@ import java.util.Optional;
public class MessageFrameTestFixture {
private static final Address DEFAUT_ADDRESS = AddressHelpers.ofValue(244259721);
public static final Address DEFAUT_ADDRESS = AddressHelpers.ofValue(244259721);
private final int maxStackSize = DEFAULT_MAX_STACK_SIZE;
private Type type = Type.MESSAGE_CALL;

@ -88,17 +88,14 @@ public final class GenesisStateTest {
assertThat(header.getParentHash()).isEqualTo(Hash.ZERO);
}
@Test
public void createFromJsonWithContract() throws Exception {
private void assertContractInvariants(
final String sourceFile, final String blockHash, final int version) throws Exception {
final GenesisState genesisState =
GenesisState.fromJson(
Resources.toString(GenesisStateTest.class.getResource("genesis3.json"), Charsets.UTF_8),
Resources.toString(GenesisStateTest.class.getResource(sourceFile), Charsets.UTF_8),
MainnetProtocolSchedule.create());
final BlockHeader header = genesisState.getBlock().getHeader();
assertThat(header.getHash())
.isEqualTo(
Hash.fromHexString(
"0xe7fd8db206dcaf066b7c97b8a42a0abc18653613560748557ab44868652a78b6"));
assertThat(header.getHash()).isEqualTo(Hash.fromHexString(blockHash));
final DefaultMutableWorldState worldState =
new DefaultMutableWorldState(new WorldStateKeyValueStorage(new InMemoryKeyValueStorage()));
@ -106,6 +103,7 @@ public final class GenesisStateTest {
final Account contract =
worldState.get(Address.fromHexString("0x3850000000000000000000000000000000000000"));
assertThat(contract.getCode()).isEqualTo(BytesValue.fromHexString(EXPECTED_CODE));
assertThat(contract.getVersion()).isEqualTo(version);
assertStorageValue(
contract,
"c2575a0e9e593c00f959f8c92f12db2869c3395a3b0502d05e2516446f71f85d",
@ -116,6 +114,18 @@ public final class GenesisStateTest {
"000000000000000000000000385ef55e292fa39cf5ffbad99f534294565519ba");
}
@Test
public void createFromJsonWithContract() throws Exception {
assertContractInvariants(
"genesis3.json", "0xe7fd8db206dcaf066b7c97b8a42a0abc18653613560748557ab44868652a78b6", 0);
}
@Test
public void createFromJsonWithVersion() throws Exception {
assertContractInvariants(
"genesis4.json", "0x3224ddae856381f5fb67492b4561ecbc0cb1e9e50e6cf3238f6e049fe95a8604", 1);
}
@Test
public void encodeOlympicBlock() throws Exception {
final GenesisState genesisState =

@ -33,6 +33,7 @@ public class WorldStateMock extends DebuggableMutableWorldState {
private final long nonce;
private final Wei balance;
private final BytesValue code;
private final int version;
private final Map<UInt256, UInt256> storage;
private static final Map<UInt256, UInt256> parseStorage(final Map<String, String> values) {
@ -47,26 +48,36 @@ public class WorldStateMock extends DebuggableMutableWorldState {
@JsonProperty("nonce") final String nonce,
@JsonProperty("balance") final String balance,
@JsonProperty("storage") final Map<String, String> storage,
@JsonProperty("code") final String code) {
@JsonProperty("code") final String code,
@JsonProperty("version") final String version) {
this.nonce = Long.decode(nonce);
this.balance = Wei.fromHexString(balance);
this.code = BytesValue.fromHexString(code);
this.storage = parseStorage(storage);
if (version != null) {
this.version = Integer.decode(version);
} else {
this.version = 0;
}
}
public long nonce() {
public long getNonce() {
return nonce;
}
public Wei balance() {
public Wei getBalance() {
return balance;
}
public BytesValue code() {
public BytesValue getCode() {
return code;
}
public Map<UInt256, UInt256> storage() {
public int getVersion() {
return version;
}
public Map<UInt256, UInt256> getStorage() {
return storage;
}
}
@ -74,10 +85,11 @@ public class WorldStateMock extends DebuggableMutableWorldState {
public static void insertAccount(
final WorldUpdater updater, final Address address, final AccountMock toCopy) {
final MutableAccount account = updater.getOrCreate(address);
account.setNonce(toCopy.nonce());
account.setBalance(toCopy.balance());
account.setCode(toCopy.code());
for (final Map.Entry<UInt256, UInt256> entry : toCopy.storage().entrySet()) {
account.setNonce(toCopy.getNonce());
account.setBalance(toCopy.getBalance());
account.setCode(toCopy.getCode());
account.setVersion(toCopy.getVersion());
for (final Map.Entry<UInt256, UInt256> entry : toCopy.getStorage().entrySet()) {
account.setStorageValue(entry.getKey(), entry.getValue());
}
}

@ -16,6 +16,7 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
import tech.pegasys.pantheon.ethereum.chain.Blockchain;
import tech.pegasys.pantheon.ethereum.core.Account;
import tech.pegasys.pantheon.ethereum.core.Address;
import tech.pegasys.pantheon.ethereum.core.AddressHelpers;
import tech.pegasys.pantheon.ethereum.core.BlockHeader;
@ -23,6 +24,7 @@ import tech.pegasys.pantheon.ethereum.core.BlockHeaderTestFixture;
import tech.pegasys.pantheon.ethereum.core.Gas;
import tech.pegasys.pantheon.ethereum.core.Hash;
import tech.pegasys.pantheon.ethereum.core.MessageFrameTestFixture;
import tech.pegasys.pantheon.ethereum.core.MutableAccount;
import tech.pegasys.pantheon.ethereum.core.Wei;
import tech.pegasys.pantheon.ethereum.core.WorldUpdater;
import tech.pegasys.pantheon.ethereum.mainnet.ConstantinopleGasCalculator;
@ -88,7 +90,9 @@ public class ExtCodeHashOperationTest {
@Test
public void shouldGetHashOfAccountCodeWhenCodeIsPresent() {
final BytesValue code = BytesValue.fromHexString("0xabcdef");
worldStateUpdater.getOrCreate(REQUESTED_ADDRESS).setCode(code);
final MutableAccount account = worldStateUpdater.getOrCreate(REQUESTED_ADDRESS);
account.setCode(code);
account.setVersion(Account.DEFAULT_VERSION);
assertThat(executeOperation(REQUESTED_ADDRESS)).isEqualTo(Hash.hash(code));
}
@ -96,7 +100,9 @@ public class ExtCodeHashOperationTest {
public void shouldZeroOutLeftMostBitsToGetAddress() {
// If EXTCODEHASH of A is X, then EXTCODEHASH of A + 2**160 is X.
final BytesValue code = BytesValue.fromHexString("0xabcdef");
worldStateUpdater.getOrCreate(REQUESTED_ADDRESS).setCode(code);
final MutableAccount account = worldStateUpdater.getOrCreate(REQUESTED_ADDRESS);
account.setCode(code);
account.setVersion(Account.DEFAULT_VERSION);
final Bytes32 value =
Words.fromAddress(REQUESTED_ADDRESS)
.asUInt256()

@ -18,6 +18,7 @@ import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import tech.pegasys.pantheon.ethereum.core.Account;
import tech.pegasys.pantheon.ethereum.core.Address;
import tech.pegasys.pantheon.ethereum.core.Hash;
import tech.pegasys.pantheon.ethereum.core.MutableAccount;
@ -482,9 +483,11 @@ public class DefaultMutableWorldStateTest {
final MutableAccount account = updater.createAccount(ADDRESS);
account.setBalance(Wei.of(100000));
account.setCode(BytesValue.of(1, 2, 3));
account.setVersion(Account.DEFAULT_VERSION);
account.setCode(BytesValue.of(3, 2, 1));
updater.commit();
assertEquals(BytesValue.of(3, 2, 1), worldState.get(ADDRESS).getCode());
assertEquals(Account.DEFAULT_VERSION, worldState.get(ADDRESS).getVersion());
assertEquals(
Hash.fromHexString("0xc14f5e30581de9155ea092affa665fad83bcd9f98e45c4a42885b9b36d939702"),
worldState.rootHash());

File diff suppressed because one or more lines are too long
Loading…
Cancel
Save