Nick Savers brilliant work on Miner:

+ the option to vaildate/mine blocks
+ nonce validation
+ calc for max gas for a block
+ difficulty calc
This commit is contained in:
romanman 2014-06-20 22:11:49 +01:00
parent 708428c01e
commit 739b1425ef
6 changed files with 317 additions and 40 deletions

View File

@ -4,11 +4,13 @@ import org.ethereum.crypto.HashUtil;
import org.ethereum.manager.WorldManager;
import org.ethereum.trie.Trie;
import org.ethereum.util.ByteUtil;
import org.ethereum.util.FastByteComparisons;
import org.ethereum.util.RLP;
import org.ethereum.util.RLPElement;
import org.ethereum.util.RLPList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.spongycastle.util.Arrays;
import org.spongycastle.util.BigIntegers;
import org.spongycastle.util.encoders.Hex;
@ -104,8 +106,9 @@ public class Block {
}
public Block getParent() {
// TODO retrieve Parent from chain
return null;
byte[] rlpEncoded = WorldManager.instance.chainDB.get(ByteUtil
.longToBytes(this.getNumber() - 1));
return new Block(rlpEncoded);
}
public byte[] getParentHash() {
@ -128,6 +131,11 @@ public class Block {
return this.header.getStateRoot();
}
public void setStateRoot(byte[] stateRoot) {
if (!parsed) parseRLP();
this.header.setStateRoot(stateRoot);
}
public byte[] getTxTrieRoot() {
if (!parsed) parseRLP();
return this.header.getTxTrieRoot();
@ -154,7 +162,7 @@ public class Block {
}
public boolean isGenesis() {
return this.getNumber() == 0;
return this.getNumber() == Genesis.NUMBER;
}
public long getGasLimit() {
@ -177,6 +185,11 @@ public class Block {
return this.header.getNonce();
}
public void setNonce(byte[] nonce) {
this.header.setNonce(nonce);
rlpEncoded = null;
}
public Trie getTxsState() {
return this.txsState;
}
@ -304,13 +317,20 @@ public class Block {
public boolean isValid() {
boolean isValid = true;
if(!this.isGenesis()) {
// verify difficulty meets requirements
//isValid = this.getDifficulty() == this.calcDifficulty();
isValid = this.getDifficulty() == this.calcDifficulty();
// verify nonce meest difficulty requirements
isValid = this.validateNonce();
// verify gasLimit meets requirements
//isValid = this.getGasLimit() == this.calcGasLimit();
isValid = this.getGasLimit() == this.calcGasLimit();
// verify timestamp meets requirements
//isValid = this.getTimestamp() > this.getParent().getTimestamp();
isValid = this.getTimestamp() > this.getParent().getTimestamp();
// verify extraData doesn't exceed 1024 bytes
isValid = this.getExtraData() == null || this.getExtraData().length <= 1024;
}
if(!isValid)
logger.warn("!!!Invalid block!!!");
return isValid;
}
@ -324,7 +344,7 @@ public class Block {
return Genesis.GAS_LIMIT;
else {
Block parent = this.getParent();
return Math.max(MIN_GAS_LIMIT, (parent.header.getGasLimit() * (1024 - 1) + (parent.header.getGasUsed() * 6 / 5)) / 1024);
return Math.max(MIN_GAS_LIMIT, (parent.getGasLimit() * (1024 - 1) + (parent.getGasUsed() * 6 / 5)) / 1024);
}
}
@ -344,17 +364,26 @@ public class Block {
}
}
/**
* Verify that block is valid for its difficulty
*
* @param block
* @param difficulty
* @param testNonce
* @return
*/
public boolean validateNonce() {
BigInteger max = BigInteger.valueOf(2).pow(256);
byte[] target = BigIntegers.asUnsignedByteArray(32,
max.divide(new BigInteger(1, this.getDifficulty())));
byte[] hash = HashUtil.sha3(this.getEncodedWithoutNonce());
byte[] concat = Arrays.concatenate(hash, this.getNonce());
byte[] result = HashUtil.sha3(concat);
return FastByteComparisons.compareTo(result, 0, 32, target, 0, 32) < 0;
}
public byte[] getEncoded() {
if(rlpEncoded == null) {
// TODO: Alternative clean way to encode, using RLP.encode() after it's optimized
// Object[] header = new Object[] { parentHash, unclesHash, coinbase,
// stateRoot, txTrieRoot, difficulty, number, minGasPrice,
// gasLimit, gasUsed, timestamp, extraData, nonce };
// Object[] transactions = this.getTransactionsList().toArray();
// Object[] uncles = this.getUncleList().toArray();
// return RLP.encode(new Object[] { header, transactions, uncles });
byte[] header = this.header.getEncoded();
byte[] transactions = RLP.encodeList();
byte[] uncles = RLP.encodeList();
@ -363,4 +392,13 @@ public class Block {
}
return rlpEncoded;
}
public byte[] getEncodedWithoutNonce() {
if (!parsed) parseRLP();
byte[] header = this.header.getEncodedWithoutNonce();
byte[] transactions = RLP.encodeList();
byte[] uncles = RLP.encodeList();
return RLP.encodeList(header, transactions, uncles);
}
}

View File

@ -174,6 +174,14 @@ public class BlockHeader {
}
public byte[] getEncoded() {
return this.getEncoded(true); // with nonce
}
public byte[] getEncodedWithoutNonce() {
return this.getEncoded(false);
}
public byte[] getEncoded(boolean withNonce) {
byte[] parentHash = RLP.encodeElement(this.parentHash);
byte[] unclesHash = RLP.encodeElement(this.unclesHash);
byte[] coinbase = RLP.encodeElement(this.coinbase);
@ -186,11 +194,16 @@ public class BlockHeader {
byte[] gasUsed = RLP.encodeBigInteger(BigInteger.valueOf(this.gasUsed));
byte[] timestamp = RLP.encodeBigInteger(BigInteger.valueOf(this.timestamp));
byte[] extraData = RLP.encodeElement(this.extraData);
if(withNonce) {
byte[] nonce = RLP.encodeElement(this.nonce);
return RLP.encodeList(parentHash, unclesHash, coinbase,
stateRoot, txTrieRoot, difficulty, number,
minGasPrice, gasLimit, gasUsed, timestamp, extraData, nonce);
} else {
return RLP.encodeList(parentHash, unclesHash, coinbase,
stateRoot, txTrieRoot, difficulty, number,
minGasPrice, gasLimit, gasUsed, timestamp, extraData);
}
}
private StringBuffer toStringBuff = new StringBuffer();
@ -201,7 +214,7 @@ public class BlockHeader {
toStringBuff.append(" parentHash=" + ByteUtil.toHexString(parentHash)).append("\n");
toStringBuff.append(" unclesHash=" + ByteUtil.toHexString(unclesHash)).append("\n");
toStringBuff.append(" coinbase=" + ByteUtil.toHexString(coinbase)).append("\n");
toStringBuff.append(" stateHash=" + ByteUtil.toHexString(stateRoot)).append("\n");
toStringBuff.append(" stateRoot=" + ByteUtil.toHexString(stateRoot)).append("\n");
toStringBuff.append(" txTrieHash=" + ByteUtil.toHexString(txTrieRoot)).append("\n");
toStringBuff.append(" difficulty=" + ByteUtil.toHexString(difficulty)).append("\n");
toStringBuff.append(" number=" + number).append("\n");
@ -218,7 +231,7 @@ public class BlockHeader {
toStringBuff.append(" parentHash=" + ByteUtil.toHexString(parentHash)).append("");
toStringBuff.append(" unclesHash=" + ByteUtil.toHexString(unclesHash)).append("");
toStringBuff.append(" coinbase=" + ByteUtil.toHexString(coinbase)).append("");
toStringBuff.append(" stateHash=" + ByteUtil.toHexString(stateRoot)).append("");
toStringBuff.append(" stateRoot=" + ByteUtil.toHexString(stateRoot)).append("");
toStringBuff.append(" txTrieHash=" + ByteUtil.toHexString(txTrieRoot)).append("");
toStringBuff.append(" difficulty=" + ByteUtil.toHexString(difficulty)).append("");
toStringBuff.append(" number=" + number).append("");

View File

@ -0,0 +1,75 @@
package org.ethereum.core;
import java.math.BigInteger;
import org.ethereum.crypto.HashUtil;
import org.ethereum.util.ByteUtil;
import org.ethereum.util.FastByteComparisons;
import org.spongycastle.util.Arrays;
import org.spongycastle.util.BigIntegers;
/**
* The Miner performs the proof-of-work needed for a valid block
*
* The mining proof-of-work (PoW) exists as a cryptographically secure nonce
* that proves beyond reasonable doubt that a particular amount of computation
* has been expended in the determination of some token value n.
* It is utilised to enforce the blockchain security by giving meaning
* and credence to the notion of difficulty (and, by extension, total difficulty).
*
* However, since mining new blocks comes with an attached reward,
* the proof-of-work not only functions as a method of securing confidence
* that the blockchain will remain canonical into the future, but also as
* a wealth distribution mechanism.
*
* See Yellow Paper: http://www.gavwood.com/Paper.pdf (chapter 11.5 Mining Proof-of-Work)
*/
public class Miner {
/**
* Adds a nonce to given block which complies with the given difficulty
*
* For the PoC series, we use a simplified proof-of-work.
* This is not ASIC resistant and is meant merely as a placeholder.
* It utilizes the bare SHA3 hash function to secure the block chain by requiring
* the SHA3 hash of the concatenation of the nonce and the headers SHA3 hash to be
* sufficiently low. It is formally defined as PoW:
*
* PoW(H, n) BE(SHA3(SHA3(RLP(H!n)) n))
*
* where:
* RLP(H!n) is the RLP encoding of the block header H, not including the
* final nonce component;
* SHA3 is the SHA3 hash function accepting an arbitrary length series of
* bytes and evaluating to a series of 32 bytes (i.e. 256-bit);
* n is the nonce, a series of 32 bytes;
* o is the series concatenation operator;
* BE(X) evaluates to the value equal to X when interpreted as a
* big-endian-encoded integer.
*
* @param newBlock without a valid nonce
* @param difficulty - the mining difficulty
* @return true if valid nonce has been added to the block
*/
public boolean mine(Block newBlock, byte[] difficulty) {
BigInteger max = BigInteger.valueOf(2).pow(256);
byte[] target = BigIntegers.asUnsignedByteArray(32,
max.divide(new BigInteger(1, difficulty)));
byte[] hash = HashUtil.sha3(newBlock.getEncodedWithoutNonce());
byte[] testNonce = new byte[32];
byte[] concat;
while(ByteUtil.increment(testNonce)) {
concat = Arrays.concatenate(hash, testNonce);
byte[] result = HashUtil.sha3(concat);
if(FastByteComparisons.compareTo(result, 0, 32, target, 0, 32) < 0) {
newBlock.setNonce(testNonce);
// System.out.println(Hex.toHexString(newBlock.getEncoded()));
return true;
}
}
return false; // couldn't find a valid nonce
}
}

View File

@ -3,6 +3,7 @@ package org.ethereum.util;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.util.Arrays;
import org.spongycastle.util.encoders.Hex;
@ -37,6 +38,10 @@ public class ByteUtil {
return bytes;
}
public static byte[] longToBytes(long l) {
return ByteBuffer.allocate(8).putLong(l).array();
}
public static String toHexString(byte[] data){
if (data == null) return "null";
else return Hex.toHexString(data);
@ -155,6 +160,16 @@ public class ByteUtil {
return baos.toByteArray();
}
public static byte[] stripLeadingZeroes(byte[] data){
if (data == null) return null;
@ -175,4 +190,19 @@ public class ByteUtil {
return result;
}
/**
* increment byte array as a number until max is reached
*/
public static boolean increment(byte[] bytes) {
final int startIndex = 0;
int i;
for (i = bytes.length-1; i >= startIndex; i--) {
bytes[i]++;
if (bytes[i] != 0)
break;
}
// we return false when all bytes are 0 again
return (i >= startIndex || bytes[startIndex] != 0);
}
}

View File

@ -2,7 +2,9 @@ package org.ethereum.core;
import java.math.BigInteger;
import org.ethereum.manager.WorldManager;
import org.ethereum.net.message.StaticMessages;
import org.ethereum.util.ByteUtil;
import org.spongycastle.util.encoders.Hex;
import org.ethereum.core.Block;
import org.ethereum.core.Genesis;
@ -163,28 +165,40 @@ public class BlockTest {
@Test
public void testCalcDifficulty() {
byte[] diffBytes = Genesis.getInstance().calcDifficulty();
BigInteger difficulty = new BigInteger(1, diffBytes);
Block genesis = Genesis.getInstance();
BigInteger difficulty = new BigInteger(1, genesis.calcDifficulty());
System.out.println("Genesis difficulty = " + difficulty.toString());
assertEquals(new BigInteger(1, Genesis.DIFFICULTY), difficulty);
// Storing genesis because the parent needs to be in the DB for calculation.
WorldManager.instance.chainDB.put(ByteUtil.longToBytes(Genesis.NUMBER),
genesis.getEncoded());
Block block1 = new Block(Hex.decode(block_1));
diffBytes = block1.calcDifficulty();
difficulty = new BigInteger(1, diffBytes);
System.out.println("Block#1 difficulty = " + difficulty.toString());
assertEquals(new BigInteger(""), difficulty);
BigInteger calcDifficulty = new BigInteger(1, block1.calcDifficulty());
BigInteger actualDifficulty = new BigInteger(1, block1.getDifficulty());
System.out.println("Block#1 actual difficulty = " + actualDifficulty.toString());
System.out.println("Block#1 calculated difficulty = " + calcDifficulty.toString());
assertEquals(actualDifficulty, calcDifficulty);
}
@Test
public void testCalcGasLimit() {
long gasLimit = Genesis.getInstance().calcGasLimit();
Block genesis = Genesis.getInstance();
long gasLimit = genesis.calcGasLimit();
System.out.println("Genesis gasLimit = " + gasLimit);
assertEquals(Genesis.GAS_LIMIT, gasLimit);
// Storing genesis because the parent needs to be in the DB for calculation.
WorldManager.instance.chainDB.put(ByteUtil.longToBytes(Genesis.NUMBER),
genesis.getEncoded());
// Test with block
Block block1 = new Block(Hex.decode(block_1));
gasLimit = block1.calcGasLimit();
System.out.println("Block#1 gasLimit = " + gasLimit);
assertEquals(999023, gasLimit);
long calcGasLimit = block1.calcGasLimit();
long actualGasLimit = block1.getGasLimit();
System.out.println("Block#1 actual gasLimit = " + actualGasLimit);
System.out.println("Block#1 calculated gasLimit = " + calcGasLimit);
assertEquals(actualGasLimit, calcGasLimit);
}
}

View File

@ -0,0 +1,107 @@
package org.ethereum.core;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import org.ethereum.util.ByteUtil;
import org.ethereum.util.FastByteComparisons;
import org.junit.Test;
import org.spongycastle.util.BigIntegers;
import org.spongycastle.util.encoders.Hex;
public class MinerTest {
// Example block#32 from Poc5 chain - rlpEncoded without nonce
private String rlpWithoutNonce = "f894f890a00a312c2b0a8f125c60a3976b6e508e740e095eb59943988d9bbfb8"
+ "aa43922e31a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d4934794e559de5527492bcb42ec68d07df0742a98ec3f1ea050188ab86bdf164ac90eb2835a04a8930aae5393c3a2ef1166fb95028f9456b880833ee248208609184e72a000830eca0080845387fd2080c0c0";
@Test
public void testMine() {
boolean miningTestEnabled = false;
if(miningTestEnabled) {
Block block = createBlock();
assertEquals(rlpWithoutNonce, Hex.toHexString(block.getEncodedWithoutNonce()));
System.out.println("Searching for nonce of following block: \n" + block.toString());
Miner miner = new Miner();
boolean mined = miner.mine(block, block.getDifficulty());
assertTrue(mined);
boolean valid = block.validateNonce();
assertTrue(valid);
// expectedHash is the actual hash from block#32 in PoC5 chain based on nonce below
String expectedHash = "ce7201f6cc5eb1a6f35c7dda8acda111647a0f8a5bf0518e46579b03e29fe14b";
assertEquals(expectedHash, Hex.toHexString(block.getHash()));
// expectedNonce is the actual nonce from block#32 in Poc5 chain
String expectedNonce = "0000000000000000000000000000000000000000000000001f52ebb192c4ea97"; // from Poc5 chain
// Actual is "000000000000000000000000000000000000000000000000000000000098cc15"
// but that might also be a valid nonce in compliance with PoW(H!n, n) < (2^256 / difficulty)
assertEquals(expectedNonce, Hex.toHexString(block.getNonce()));
}
}
/**
* Produces a block equal to block#32 on PoC5 testnet (protocol 19)
* Where nonce was '0000000000000000000000000000000000000000000000001f52ebb192c4ea97'
* and resulting hash 'ce7201f6cc5eb1a6f35c7dda8acda111647a0f8a5bf0518e46579b03e29fe14b'
*/
private Block createBlock() {
byte[] parentHash = Hex.decode("0a312c2b0a8f125c60a3976b6e508e740e095eb59943988d9bbfb8aa43922e31");
byte[] unclesHash = Hex.decode("1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347");
byte[] coinbase = Hex.decode("e559de5527492bcb42ec68d07df0742a98ec3f1e");
byte[] difficulty = Hex.decode("3ee248");
byte[] nonce = null;
long number = 32;
long minGasPrice = 10000000000000L;
long gasLimit = 969216;
long gasUsed = 0;
long timestamp = 1401421088;
Block newBlock = new Block(parentHash, unclesHash, coinbase,
difficulty, number, minGasPrice, gasLimit, gasUsed, timestamp,
null, nonce, null, null);
// Setting stateRoot manually, because don't have state available.
newBlock.setStateRoot(Hex.decode("50188ab86bdf164ac90eb2835a04a8930aae5393c3a2ef1166fb95028f9456b8"));
return newBlock;
}
/**
* This test shows the difference between iterating over,
* and comparing byte[] vs BigInteger value.
*
* Results indicate that the former has ~15x better performance.
* Therefore this is used in the Miner.mine() method.
*/
@Test
public void testIncrementPerformance() {
boolean testEnabled = false;
if(testEnabled) {
byte[] counter1 = new byte[4];
byte[] max = ByteBuffer.allocate(4).putInt(Integer.MAX_VALUE).array();
long start1 = System.currentTimeMillis();
while(ByteUtil.increment(counter1)) {
if(FastByteComparisons.compareTo(counter1, 0, 4, max, 0, 4) == 0) {
break;
}
}
System.out.println(System.currentTimeMillis() - start1 + "ms to reach: " + Hex.toHexString(counter1));
BigInteger counter2 = BigInteger.ZERO;
long start2 = System.currentTimeMillis();
while(true) {
if(counter2.compareTo(BigInteger.valueOf(Integer.MAX_VALUE)) == 0) {
break;
}
counter2 = counter2.add(BigInteger.ONE);
}
System.out.println(System.currentTimeMillis() - start2 + "ms to reach: " + Hex.toHexString(BigIntegers.asUnsignedByteArray(4, counter2)));
}
}
}