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

Support for Ethereum Recovery Proposals #1004

Closed
wants to merge 13 commits into from
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
/*
* Copyright (c) [2016] [ <ether.camp> ]
* This file is part of the ethereumJ library.
*
* The ethereumJ library is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The ethereumJ library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with the ethereumJ library. If not, see <http://www.gnu.org/licenses/>.
*/
package org.ethereum.config.blockchain;

import org.apache.commons.lang3.tuple.Pair;
import org.ethereum.config.BlockchainConfig;
import org.ethereum.core.Block;
import org.ethereum.core.BlockHeader;
import org.ethereum.core.Repository;
import org.ethereum.core.Transaction;
import org.ethereum.erp.ErpExecutor;
import org.ethereum.erp.ErpLoader;
import org.ethereum.erp.ErpLoader.ErpMetadata;
import org.ethereum.erp.StateChangeObject;
import org.ethereum.validator.BlockHeaderRule;
import org.ethereum.validator.BlockHeaderValidator;
import org.ethereum.validator.ExtraDataPresenceRule;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.math.BigInteger;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.function.Function;

import static java.util.stream.Collectors.toMap;

/**
* Created by Dan Phifer, 2018-02-01.
*/
public class ErpConfig extends FrontierConfig /* TODO: Is FrontierConfig correct? */ {
Copy link
Contributor

Choose a reason for hiding this comment

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

I guess ErpConfig should extend ByzantiumConfig. This assumption is based on further 6_000_000 block number used in the context. Check ByzantiumConfig implementation for details


private final long EXTRA_DATA_AFFECTS_BLOCKS_NUMBER = 10;
public static final Logger logger = LoggerFactory.getLogger("config");
Copy link
Contributor

Choose a reason for hiding this comment

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

Use general logger instead of config


private BlockchainConfig parent;
private Map<Long, ErpMetadata> erpDataByTargetBlock;
private ErpLoader erpLoader;
private ErpExecutor erpExecutor;

public ErpConfig() {
this(new HomesteadConfig(), new ErpLoader("/erps"), new ErpExecutor());
}

public ErpConfig(BlockchainConfig parent, ErpLoader erpLoader, ErpExecutor erpExecutor) {
this.erpLoader = erpLoader;
this.erpExecutor = erpExecutor;
this.parent = parent;
this.constants = parent.getConstants();

try {
initErpConfig();
} catch (IOException e) {
// TODO: not sure what to do here.
Copy link
Contributor

Choose a reason for hiding this comment

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

throwing RuntimeException should be enough here

logger.error("Failed to load the ERPConfig", e);
throw new RuntimeException(e);
}
}

void initErpConfig() throws IOException {
// load the config block numbers
final Collection<ErpMetadata> allErps = erpLoader.loadErpMetadata();
this.erpDataByTargetBlock = allErps
.stream()
.collect(toMap(ErpMetadata::getTargetBlock, Function.identity()));

logger.info("Found %d ERPs", allErps.size());

// add the header validators for each known ERP
final List<Pair<Long, BlockHeaderValidator>> headerValidators = headerValidators();
allErps.forEach(erpMetadata -> {
BlockHeaderRule rule = new ExtraDataPresenceRule(erpMetadata.getErpMarker(), true);
headerValidators.add(Pair.of(erpMetadata.getTargetBlock(), new BlockHeaderValidator(rule)));
});
}

/**
* Miners should include marker for initial 10 blocks. Either "dao-hard-fork" or ""
*/
@Override
public byte[] getExtraData(byte[] minerExtraData, long blockNumber) {
// TODO: is EXTRA_DATA_AFFECTS_BLOCKS_NUMBER needed?
Copy link
Contributor

Choose a reason for hiding this comment

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

You may use this code if blockNumber belongs to one of the first 10 blocks, otherwise call to parent.getExtraData. Did I get you?

Copy link
Author

Choose a reason for hiding this comment

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

I think so. I saw in the DaoHFConfig that the extra data was applied not just at the target block, but for an a total of 10 blocks. I wasn't sure why that was done or if the same logic would be needed in this case.

Copy link
Contributor

Choose a reason for hiding this comment

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

I suppose it was done to check if miners accept the fork. Adding data in ten blocks in a row is more confident than just one

final ErpMetadata erpMetadata = erpDataByTargetBlock.get(blockNumber);
return erpMetadata != null
? erpMetadata.getErpMarker()
: minerExtraData;
}

@Override
public void hardForkTransfers(Block block, Repository repo) {
final ErpMetadata erpMetadata = erpDataByTargetBlock.get(block.getNumber());
if (erpMetadata != null) {
logger.info("Found ERP {} for block {}", erpMetadata.getId(), erpMetadata.getTargetBlock());
doHardForkTransfers(erpMetadata, repo);
}
}

void doHardForkTransfers(ErpMetadata erpMetadata, Repository repo) {
final StateChangeObject sco;
try {
sco = erpLoader.loadStateChangeObject(erpMetadata);
} catch (IOException e) {
logger.error("Failed to load state change object for {}", erpMetadata.getId(), e);
throw new RuntimeException("Failed to load state change object for " + erpMetadata.getId(), e);
}

// TODO: Is this the right way to apply changes in batch?
Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, it's implemented in the right way

final Repository track = repo.startTracking();
try {
erpExecutor.applyStateChanges(sco, track);
track.commit();
logger.info("Successfully applied ERP '{}' to block {}", erpMetadata.getId(), erpMetadata.getTargetBlock());
}
catch (ErpExecutor.ErpExecutionException e) {
track.rollback();
logger.error("Failed to apply ERP '{}' to block {}", erpMetadata.getId(), erpMetadata.getTargetBlock(), e);
}
catch (Exception e) {
track.rollback();
logger.error("Failed to apply ERP '{}' to block {}", erpMetadata.getId(), erpMetadata.getTargetBlock(), e);
throw e;
}
finally {
track.close();
Copy link
Contributor

Choose a reason for hiding this comment

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

don't have to do this, just omit

Copy link
Author

Choose a reason for hiding this comment

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

I was thinking it would actually be better to leave it. The startTracking method on the Repository interface returns another Repository; it makes no promise about the implementation. Whether the close method is a no-op or not is an implementation detail of the returned class, right?

Copy link
Contributor

Choose a reason for hiding this comment

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

ok, let's keep it there

}
}


//////////////////////////////////////////////
// TODO: These methods we included in DaoHFConfig, so I have included them here as well.
Copy link
Contributor

Choose a reason for hiding this comment

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

You don't have to implement them since they are implemented by parent class

// I don't know enough about the Config setup to understand if they are needed or
// if other methods are needed as well.
//////////////////////////////////////////////


@Override
public BigInteger calcDifficulty(BlockHeader curBlock, BlockHeader parent) {
return this.parent.calcDifficulty(curBlock, parent);
}

@Override
public long getTransactionCost(Transaction tx) {
return parent.getTransactionCost(tx);
}

@Override
public boolean acceptTransactionSignature(Transaction tx) {
return parent.acceptTransactionSignature(tx);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,6 @@ public MainNetConfig() {
add(2_463_000, new Eip150HFConfig(new DaoHFConfig()));
add(2_675_000, new Eip160HFConfig(new DaoHFConfig()));
add(4_370_000, new ByzantiumConfig(new DaoHFConfig()));
add(6_000_000, new ErpConfig());
Copy link
Contributor

Choose a reason for hiding this comment

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

Take a look at JsonNetConfig you might want to handle ErpConfig there too

}
}
86 changes: 86 additions & 0 deletions ethereumj-core/src/main/java/org/ethereum/erp/ErpExecutor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* Copyright (c) [2016] [ <ether.camp> ]
* This file is part of the ethereumJ library.
*
* The ethereumJ library is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The ethereumJ library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with the ethereumJ library. If not, see <http://www.gnu.org/licenses/>.
*/
package org.ethereum.erp;

import org.ethereum.core.Repository;
import org.ethereum.erp.StateChangeObject.StateChangeAction;

import java.util.Arrays;

import static org.ethereum.util.ByteUtil.EMPTY_BYTE_ARRAY;
import static org.ethereum.util.ByteUtil.toHexString;

/**
* The ERP Executor applies State Change Actions to a Repository.
*/
public class ErpExecutor {
public static final String WEI_TRANSFER = "weiTransfer";
public static final String STORE_CODE = "storeCode";

public static class ErpExecutionException extends Exception {
ErpExecutionException(Throwable cause) {
super(cause);
}
}

public void applyStateChanges(StateChangeObject sco, Repository repo) throws ErpExecutionException {
try {
for (StateChangeAction action : sco.actions) {
applyStateChangeAction(action, repo);
Copy link
Contributor

Choose a reason for hiding this comment

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

Applying code could be a part of StateChangeAction. It would be more readable then

Copy link
Author

Choose a reason for hiding this comment

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

Ok, I will change to a custom deserializer, which will remove the RawStateChangeObject and will allow me to have separate action classes for each action type. so the interface will be action.applyStateChange(repo)

}
}
catch (IllegalArgumentException | UnsupportedOperationException e) {
throw new ErpExecutionException(e);
}
}

void applyStateChangeAction(StateChangeAction action, Repository repo) {
switch (action.type) {
case WEI_TRANSFER:
applyWeiTransfer(action, repo);
break;
case STORE_CODE:
applyStoreCode(action, repo);
break;
default:
throw new UnsupportedOperationException("ERP action type not supported. " + action.type);
}
}

void applyStoreCode(StateChangeAction action, Repository repo) {
if (Arrays.equals(action.toAddress, EMPTY_BYTE_ARRAY))
throw new IllegalArgumentException("storeCode cannot store code at an empty address");

if (!Arrays.equals(action.expectedCodeHash, repo.getCodeHash(action.toAddress)))
throw new IllegalStateException(String.format("ERP storeCode did not find the expected hash. Expected %s, found %s", toHexString(action.expectedCodeHash), toHexString(repo.getCodeHash(action.toAddress))));

repo.saveCode(action.toAddress, action.code);
}

void applyWeiTransfer(StateChangeAction action, Repository repo) {
if (Arrays.equals(action.toAddress, EMPTY_BYTE_ARRAY))
throw new IllegalArgumentException("weiTransfer cannot transfer to an empty address");

// is this needed here? Seems like this check should happen at a lower level (i.e. in the repo)
if (repo.getBalance(action.fromAddress).compareTo(action.valueInWei) < 0)
throw new IllegalStateException(String.format("ERP insufficient balance for weiTransfer. Sender balance of %s less than transfer amount of %s", repo.getBalance(action.fromAddress).toString(), action.valueInWei.toString()));

repo.addBalance(action.fromAddress, action.valueInWei.negate());
repo.addBalance(action.toAddress, action.valueInWei);
}
}
128 changes: 128 additions & 0 deletions ethereumj-core/src/main/java/org/ethereum/erp/ErpLoader.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/*
* Copyright (c) [2016] [ <ether.camp> ]
* This file is part of the ethereumJ library.
*
* The ethereumJ library is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The ethereumJ library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with the ethereumJ library. If not, see <http://www.gnu.org/licenses/>.
*/
package org.ethereum.erp;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.ethereum.util.ByteUtil;

import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.net.URL;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;

public class ErpLoader {
Copy link
Contributor

Choose a reason for hiding this comment

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

I guess it could be simplified somehow. For example, there is no need for extra-classes like RawStateChangeObject

public static final String SCO_EXTENSION = ".sco.json";
private static final FilenameFilter SCO_FILE_FILTER = (dir, name) -> name.endsWith(SCO_EXTENSION);
private final String resourceDir;

public ErpLoader(String resourceDir) {
this.resourceDir = resourceDir;
}

/**
* A lightweight object that can be held in memory
*/
public static class ErpMetadata {
Long targetBlock;
File resourceFile;
String erpId;
byte[] erpMarker;

ErpMetadata(String erpId, Long targetBlock, File resourceFile) {
this.erpId = erpId;
this.targetBlock = targetBlock;
this.resourceFile = resourceFile;
this.erpMarker = ByteUtil.hexStringToBytes(asciiToHex(erpId));
}

private static String asciiToHex(String asciiValue){
char[] chars = asciiValue.toCharArray();
StringBuilder hex = new StringBuilder();
for (int i = 0; i < chars.length; i++) {
hex.append(Integer.toHexString((int) chars[i]));
}
return hex.toString();
}

public byte[] getErpMarker() {
return erpMarker;
}

public String getId() {
return erpId;
}

public long getTargetBlock() {
return targetBlock;
}
}

/**
* This method returns a collection of lightweight objects, but it parses each
* file to pull out the erpId and the target block as well as sanity check the actions
*
* @return A collection of ERPs that are available
* @throws IOException if any of the ERP files could not be loaded
*/
public Collection<ErpMetadata> loadErpMetadata() throws IOException {
// this is somewhat inefficient, but the goal is to ensure that all important
// data is loaded from the SCO object itself
List<ErpMetadata> allMetadata = new LinkedList<>();
for (File f : loadERPResourceFiles(this.resourceDir)) {
final StateChangeObject sco = loadStateChangeObject(f);
allMetadata.add(new ErpMetadata(sco.erpId, sco.targetBlock, f));
}
return allMetadata;
}

/**
* Loads the StateChangeObject from a file synchronously
* @param metadata The ERP metadata object
* @return A StateChangeObject that can be executed against a repo by the ErpExecutor
* @throws IOException if the StateChangeObject cannot be loaded.
*/
public StateChangeObject loadStateChangeObject(ErpMetadata metadata) throws IOException {
return loadStateChangeObject(metadata.resourceFile);
}

File[] loadERPResourceFiles(String erpResourceDir) throws IOException {
URL url = getClass().getResource(erpResourceDir);

File[] files = url != null
? new File(url.getPath()).listFiles(SCO_FILE_FILTER)
: null;

// an empty array is ok (which would mean there are no files in the directory)
if (files == null)
throw new IOException("The specified resource directory does not exists: " + erpResourceDir);

return files;
}

StateChangeObject loadStateChangeObject(File f) throws IOException {
return StateChangeObject.parse(loadRawStateChangeObject(f));
}

RawStateChangeObject loadRawStateChangeObject(File f) throws IOException {
ObjectMapper objectMapper = new ObjectMapper();
return objectMapper.readValue(f, RawStateChangeObject.class);
}
}
Loading