3
0
mirror of https://github.com/Qortal/altcoinj.git synced 2025-02-11 17:55:53 +00:00

Add unit tests for AuxPoW header validation.

This commit is contained in:
Ross Nicoll 2015-06-01 23:20:15 +01:00
parent 36753e5112
commit 258792c803
3 changed files with 362 additions and 30 deletions

View File

@ -313,12 +313,11 @@ public class AuxPoW extends ChildMessage implements Serializable {
return false;
}
// Check that the chain merkle root is in the coinbase
Sha256Hash nRootHash = getChainMerkleBranch().calculateMerkleRoot(hashAuxBlock);
final byte[] vchRootHash = nRootHash.getBytes();
//std::reverse(vchRootHash.begin(), vchRootHash.end()); // correct endian// correct endian
// Check that we are in the parent block merkle tree
// Check that the coinbase transaction is in the merkle tree of the
// parent block header
if (!getCoinbaseBranch().calculateMerkleRoot(getCoinbase().getHash()).equals(parentBlockHeader.getMerkleRoot())) {
if (throwException) {
throw new VerificationException("Aux POW merkle root incorrect");
@ -326,31 +325,35 @@ public class AuxPoW extends ChildMessage implements Serializable {
return false;
}
if (this.getCoinbase().getInputs().isEmpty()) {
throw new VerificationException("Coinbase transaction has no inputs");
}
// Check that the chain merkle root is in the coinbase
final byte[] script = this.getCoinbase().getInput(0).getScriptBytes();
// Check that the same work is not submitted twice to our chain.
//
int pcHead = -1;
// Check that the same work is not submitted twice to our chain, by
// confirming that the child block hash is in the coinbase merkle tree
int pcHeader = -1;
int pc = -1;
for (int scriptIdx = 0; scriptIdx < script.length; scriptIdx++) {
if (arrayMatch(script, scriptIdx, MERGED_MINING_HEADER)) {
// Enforce only one chain merkle root by checking that a single instance of the merged
// mining header exists just before.
if (pcHead >= 0) {
if (pcHeader >= 0) {
if (throwException) {
throw new VerificationException("Multiple merged mining headers in coinbase");
}
return false;
}
pcHead = scriptIdx;
}
if (arrayMatch(script, scriptIdx, vchRootHash)) {
pcHeader = scriptIdx;
} else if (arrayMatch(script, scriptIdx, vchRootHash)) {
pc = scriptIdx;
}
}
if (-1 == pcHead) {
if (-1 == pcHeader) {
if (throwException) {
throw new VerificationException("MergedMiningHeader missing from parent coinbase");
}
@ -364,7 +367,7 @@ public class AuxPoW extends ChildMessage implements Serializable {
return false;
}
if (pcHead + MERGED_MINING_HEADER.length != pc) {
if (pcHeader + MERGED_MINING_HEADER.length != pc) {
if (throwException) {
throw new VerificationException("Merged mining header is not just before chain merkle root");
}
@ -449,7 +452,22 @@ public class AuxPoW extends ChildMessage implements Serializable {
/**
* Get the chain ID from a block header.
*/
protected long getChainID(final Block blockHeader) {
public static long getChainID(final Block blockHeader) {
return blockHeader.getVersion() / AltcoinBlock.BLOCK_VERSION_CHAIN_START;
}
/**
* Set the merkle branch used to connect the coinbase transaction to the
* parent block header.
*/
public void setCoinbaseBranch(final MerkleBranch merkleBranch) {
this.coinbaseBranch = merkleBranch;
}
/**
* Set the parent chain block header.
*/
public void setParentBlockHeader(final AltcoinBlock header) {
this.parentBlockHeader = header;
}
}

View File

@ -48,15 +48,15 @@ public class MerkleBranch extends ChildMessage implements Serializable {
// can properly keep track of optimal encoded size
private transient int optimalEncodingMessageSize;
private List<Sha256Hash> branchHashes;
private List<Sha256Hash> hashes;
private long index;
public MerkleBranch(NetworkParameters params, @Nullable ChildMessage parent) {
super(params);
setParent(parent);
this.branchHashes = new ArrayList<Sha256Hash>();
this.index = 0;
this.hashes = new ArrayList<Sha256Hash>();
this.index = 0;
}
/**
@ -86,7 +86,7 @@ public class MerkleBranch extends ChildMessage implements Serializable {
super(params);
setParent(parent);
this.branchHashes = hashes;
this.hashes = hashes;
this.index = branchSideMask;
}
@ -111,19 +111,19 @@ public class MerkleBranch extends ChildMessage implements Serializable {
final int hashCount = (int) readVarInt();
optimalEncodingMessageSize += VarInt.sizeOf(hashCount);
branchHashes = new ArrayList<Sha256Hash>(hashCount);
hashes = new ArrayList<Sha256Hash>(hashCount);
for (int hashIdx = 0; hashIdx < hashCount; hashIdx++) {
branchHashes.add(readHash());
hashes.add(readHash());
}
optimalEncodingMessageSize += 32 * hashCount;
index = readUint32();
setIndex(readUint32());
optimalEncodingMessageSize += 4;
}
@Override
protected void bitcoinSerializeToStream(OutputStream stream) throws IOException {
stream.write(new VarInt(branchHashes.size()).encode());
for (Sha256Hash hash: branchHashes) {
stream.write(new VarInt(hashes.size()).encode());
for (Sha256Hash hash: hashes) {
stream.write(Utils.reverseBytes(hash.getBytes()));
}
Utils.uint32ToByteStreamLE(index, stream);
@ -137,7 +137,7 @@ public class MerkleBranch extends ChildMessage implements Serializable {
byte[] target = reverseBytes(leaf.getBytes());
long mask = index;
for (Sha256Hash hash: branchHashes) {
for (Sha256Hash hash: hashes) {
target = (mask & 1) == 0
? doubleDigestTwoBuffers(target, 0, 32, reverseBytes(hash.getBytes()), 0, 32)
: doubleDigestTwoBuffers(reverseBytes(hash.getBytes()), 0, 32, target, 0, 32);
@ -150,7 +150,7 @@ public class MerkleBranch extends ChildMessage implements Serializable {
* Get the hashes which make up this branch.
*/
public List<Sha256Hash> getHashes() {
return Collections.unmodifiableList(this.branchHashes);
return Collections.unmodifiableList(this.hashes);
}
/**
@ -162,11 +162,26 @@ public class MerkleBranch extends ChildMessage implements Serializable {
return index;
}
/**
* @param hashes the hashes to set
*/
public void setHashes(List<Sha256Hash> hashes) {
this.hashes = hashes;
}
/**
* Set the mask used to determine the sides in which hashes are applied.
*/
public void setIndex(final long newIndex) {
assert newIndex >= 0;
this.index = newIndex;
}
/**
* Get the number of hashes in this branch.
*/
public int size() {
return branchHashes.size();
return hashes.size();
}
public int getOptimalEncodingMessageSize() {
@ -214,7 +229,7 @@ public class MerkleBranch extends ChildMessage implements Serializable {
MerkleBranch input = (MerkleBranch) o;
if (!branchHashes.equals(input.branchHashes)) return false;
if (!hashes.equals(input.hashes)) return false;
if (index != input.index) return false;
return true;
@ -223,7 +238,7 @@ public class MerkleBranch extends ChildMessage implements Serializable {
@Override
public int hashCode() {
int result = 1;
result = 31 * result + branchHashes.hashCode();
result = 31 * result + hashes.hashCode();
result = 31 * result + (int) index;
return result;
}

View File

@ -1,19 +1,32 @@
package org.bitcoinj.core;
import java.math.BigInteger;
import org.altcoinj.params.DogecoinTestNet3Params;
import java.util.Arrays;
import java.util.Collections;
import org.altcoinj.core.AltcoinSerializer;
import org.altcoinj.params.DogecoinMainNetParams;
import org.junit.Test;
import static org.bitcoinj.core.Util.getBytes;
import static org.bitcoinj.core.Utils.doubleDigestTwoBuffers;
import static org.bitcoinj.core.Utils.reverseBytes;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import org.junit.Before;
import org.junit.Rule;
import org.junit.rules.ExpectedException;
/**
* AuxPoW header parsing/serialization and validation
*/
public class AuxPoWTest {
static final NetworkParameters params = DogecoinTestNet3Params.get();
static final NetworkParameters params = DogecoinMainNetParams.get();
private static final int MERKLE_ROOT_COINBASE_INDEX = 0;
@Before
public void setUp() throws Exception {
Context context = new Context(params);
}
/**
* Parse the AuxPoW header from Dogecoin block #403,931.
@ -56,4 +69,290 @@ public class AuxPoWTest {
auxpow.checkProofOfWork(new Sha256Hash("0c836b86991631d34a8a68054e2f62db919b39d1ee43c27ab3344d6aa82fa609"),
Utils.decodeCompactBits(0x1b06f8f0), true);
}
@Rule
public ExpectedException expectedEx = ExpectedException.none();
/**
* Check that a non-generate AuxPoW transaction is rejected.
*/
@Test
public void shouldRejectNonGenerateAuxPoW() throws Exception {
final byte[] auxpowAsBytes = getBytes(getClass().getResourceAsStream("auxpow_header.bin"));
final AuxPoW auxpow = new AuxPoW(params, auxpowAsBytes, (ChildMessage) null, params.getDefaultSerializer());
auxpow.getCoinbaseBranch().setIndex(0x01);
expectedEx.expect(org.bitcoinj.core.VerificationException.class);
expectedEx.expectMessage("AuxPow is not a generate");
auxpow.checkProofOfWork(new Sha256Hash("0c836b86991631d34a8a68054e2f62db919b39d1ee43c27ab3344d6aa82fa609"),
Utils.decodeCompactBits(0x1b06f8f0), true);
}
/**
* Check that block headers from the child chain are rejected as parent
* chain for AuxPoW, via checking of the chain IDs.
*/
@Test
public void shouldRejectOwnChainID() throws Exception {
byte[] payload = Util.getBytes(getClass().getResourceAsStream("dogecoin_block371337.bin"));
AltcoinSerializer serializer = (AltcoinSerializer)params.getDefaultSerializer();
final AltcoinBlock block = (AltcoinBlock)serializer.makeBlock(payload);
final AuxPoW auxpow = block.getAuxPoW();
auxpow.setParentBlockHeader((AltcoinBlock)block.cloneAsHeader());
expectedEx.expect(org.bitcoinj.core.VerificationException.class);
expectedEx.expectMessage("Aux POW parent has our chain ID");
auxpow.checkProofOfWork(new Sha256Hash("0c836b86991631d34a8a68054e2f62db919b39d1ee43c27ab3344d6aa82fa609"),
Utils.decodeCompactBits(0x1b06f8f0), true);
}
/**
* Check that where the merkle branch is far too long to use, it's rejected.
*/
@Test
public void shouldRejectVeryLongMerkleBranch() throws Exception {
final byte[] auxpowAsBytes = getBytes(getClass().getResourceAsStream("auxpow_header.bin"));
final AuxPoW auxpow = new AuxPoW(params, auxpowAsBytes, (ChildMessage) null, params.getDefaultSerializer());
auxpow.getChainMerkleBranch().setHashes(Arrays.asList(new Sha256Hash[32]));
expectedEx.expect(org.bitcoinj.core.VerificationException.class);
expectedEx.expectMessage("Aux POW chain merkle branch too long");
auxpow.checkProofOfWork(new Sha256Hash("0c836b86991631d34a8a68054e2f62db919b39d1ee43c27ab3344d6aa82fa609"),
Utils.decodeCompactBits(0x1b06f8f0), true);
}
/**
* Later steps in AuxPoW validation depend on the contents of the coinbase
* transaction. Obviously that's useless if we don't check the coinbase
* transaction is actually part of the parent chain block, so first we test
* that the transaction hash is part of the merkle tree. This test modifies
* the transaction, invalidating the hash, to confirm that it's rejected.
*/
@Test
public void shouldRejectIfCoinbaseTransactionNotInMerkleBranch() throws Exception {
final byte[] auxpowAsBytes = getBytes(getClass().getResourceAsStream("auxpow_header.bin"));
final AuxPoW auxpow = new AuxPoW(params, auxpowAsBytes, (ChildMessage) null, params.getDefaultSerializer());
auxpow.getCoinbase().clearOutputs();
expectedEx.expect(org.bitcoinj.core.VerificationException.class);
expectedEx.expectMessage("Aux POW merkle root incorrect");
auxpow.checkProofOfWork(new Sha256Hash("0c836b86991631d34a8a68054e2f62db919b39d1ee43c27ab3344d6aa82fa609"),
Utils.decodeCompactBits(0x1b06f8f0), true);
}
/**
* Ensure that in case of a malformed coinbase transaction (no inputs) it's
* caught and processed neatly.
*/
@Test
public void shouldRejectIfCoinbaseTransactionHasNoInputs() throws Exception {
final byte[] auxpowAsBytes = getBytes(getClass().getResourceAsStream("auxpow_header.bin"));
final AuxPoW auxpow = new AuxPoW(params, auxpowAsBytes, (ChildMessage) null, params.getDefaultSerializer());
// This will also break the difficulty check, but as that doesn't occur
// until the end, we can get away with it.
auxpow.getCoinbase().clearInputs();
updateMerkleRootToMatchCoinbase(auxpow);
expectedEx.expect(org.bitcoinj.core.VerificationException.class);
expectedEx.expectMessage("Coinbase transaction has no inputs");
auxpow.checkProofOfWork(new Sha256Hash("0c836b86991631d34a8a68054e2f62db919b39d1ee43c27ab3344d6aa82fa609"),
Utils.decodeCompactBits(0x1b06f8f0), true);
}
/**
* Catch the case that the merged mine header is missing from the coinbase
* transaction.
*/
@Test
public void shouldRejectIfMergedMineHeaderMissing() throws Exception {
final byte[] auxpowAsBytes = getBytes(getClass().getResourceAsStream("auxpow_header.bin"));
final AuxPoW auxpow = new AuxPoW(params, auxpowAsBytes, (ChildMessage) null, params.getDefaultSerializer());
// This will also break the difficulty check, but as that doesn't occur
// until the end, we can get away with it.
final TransactionInput in = auxpow.getCoinbase().getInput(0);
in.getScriptBytes()[4] = 0; // Break the first byte of the header
updateMerkleRootToMatchCoinbase(auxpow);
expectedEx.expect(org.bitcoinj.core.VerificationException.class);
expectedEx.expectMessage("MergedMiningHeader missing from parent coinbase");
auxpow.checkProofOfWork(new Sha256Hash("0c836b86991631d34a8a68054e2f62db919b39d1ee43c27ab3344d6aa82fa609"),
Utils.decodeCompactBits(0x1b06f8f0), true);
}
/**
* Catch the case that more than one merged mine header is present in the
* coinbase transaction (this is considered an attempt to confuse the parser).
*/
@Test
public void shouldRejectIfMergedMineHeaderDuplicated() throws Exception {
final byte[] auxpowAsBytes = getBytes(getClass().getResourceAsStream("auxpow_header.bin"));
final AuxPoW auxpow = new AuxPoW(params, auxpowAsBytes, (ChildMessage) null, params.getDefaultSerializer());
// This will also break the difficulty check, but as that doesn't occur
// until the end, we can get away with it.
final TransactionInput in = auxpow.getCoinbase().getInput(0);
final byte[] newBytes = Arrays.copyOf(in.getScriptBytes(), in.getScriptBytes().length + 4);
for (int byteIdx = 0; byteIdx < AuxPoW.MERGED_MINING_HEADER.length; byteIdx++) {
newBytes[newBytes.length - 4 + byteIdx] = AuxPoW.MERGED_MINING_HEADER[byteIdx];
}
in.setScriptBytes(newBytes);
updateMerkleRootToMatchCoinbase(auxpow);
expectedEx.expect(org.bitcoinj.core.VerificationException.class);
expectedEx.expectMessage("Multiple merged mining headers in coinbase");
auxpow.checkProofOfWork(new Sha256Hash("0c836b86991631d34a8a68054e2f62db919b39d1ee43c27ab3344d6aa82fa609"),
Utils.decodeCompactBits(0x1b06f8f0), true);
}
/**
* Catch the case that the chain merkle branch is missing from the coinbase
* transaction. The chain merkle branch is used to prove that the block was
* mined for chain or chains including this one (i.e. random proof of work
* cannot be taken from any merged-mined blockchain and reused).
*/
@Test
public void shouldRejectIfCoinbaseMissingChainMerkleRoot() throws Exception {
final byte[] auxpowAsBytes = getBytes(getClass().getResourceAsStream("auxpow_header.bin"));
final AuxPoW auxpow = new AuxPoW(params, auxpowAsBytes, (ChildMessage) null, params.getDefaultSerializer());
// This will also break the difficulty check, but as that doesn't occur
// until the end, we can get away with it.
final TransactionInput in = auxpow.getCoinbase().getInput(0);
in.getScriptBytes()[8] = 0; // Break the first byte of the chain merkle root
updateMerkleRootToMatchCoinbase(auxpow);
expectedEx.expect(org.bitcoinj.core.VerificationException.class);
expectedEx.expectMessage("Aux POW missing chain merkle root in parent coinbase");
auxpow.checkProofOfWork(new Sha256Hash("0c836b86991631d34a8a68054e2f62db919b39d1ee43c27ab3344d6aa82fa609"),
Utils.decodeCompactBits(0x1b06f8f0), true);
}
/**
* Catch the case that the chain merkle branch is not immediately after the
* merged mine header in the coinbase transaction (this is considered an
* attempt to confuse the parser).
*/
@Test
public void shouldRejectIfChainMerkleRootNotAfterHeader() throws Exception {
final byte[] auxpowAsBytes = getBytes(getClass().getResourceAsStream("auxpow_header.bin"));
final AuxPoW auxpow = new AuxPoW(params, auxpowAsBytes, (ChildMessage) null, params.getDefaultSerializer());
// This will also break the difficulty check, but as that doesn't occur
// until the end, we can get away with it.
final TransactionInput in = auxpow.getCoinbase().getInput(0);
final byte[] newBytes = Arrays.copyOf(in.getScriptBytes(), in.getScriptBytes().length + 1);
// Copy every byte after the merged-mine header forward one byte. We
// have to do this from the end of the array backwards to avoid overwriting
// the next byte to copy.
for (int byteIdx = newBytes.length - 1; byteIdx > 8; byteIdx--) {
newBytes[byteIdx] = newBytes[byteIdx - 1];
}
newBytes[8] = (byte) 0xff;
in.setScriptBytes(newBytes);
updateMerkleRootToMatchCoinbase(auxpow);
expectedEx.expect(org.bitcoinj.core.VerificationException.class);
expectedEx.expectMessage("Merged mining header is not just before chain merkle root");
auxpow.checkProofOfWork(new Sha256Hash("0c836b86991631d34a8a68054e2f62db919b39d1ee43c27ab3344d6aa82fa609"),
Utils.decodeCompactBits(0x1b06f8f0), true);
}
/**
* Catch the case that the chain merkle branch is not immediately after the
* merged mine header in the coinbase transaction (this is considered an
* attempt to confuse the parser).
*/
@Test
public void shouldRejectIfScriptBytesTooShort() throws Exception {
final byte[] auxpowAsBytes = getBytes(getClass().getResourceAsStream("auxpow_header.bin"));
final AuxPoW auxpow = new AuxPoW(params, auxpowAsBytes, (ChildMessage) null, params.getDefaultSerializer());
// This will also break the difficulty check, but as that doesn't occur
// until the end, we can get away with it.
final TransactionInput in = auxpow.getCoinbase().getInput(0);
final byte[] newBytes = Arrays.copyOf(in.getScriptBytes(), in.getScriptBytes().length - 12);
in.setScriptBytes(newBytes);
updateMerkleRootToMatchCoinbase(auxpow);
expectedEx.expect(org.bitcoinj.core.VerificationException.class);
expectedEx.expectMessage("Aux POW missing chain merkle tree size and nonce in parent coinbase");
auxpow.checkProofOfWork(new Sha256Hash("0c836b86991631d34a8a68054e2f62db919b39d1ee43c27ab3344d6aa82fa609"),
Utils.decodeCompactBits(0x1b06f8f0), true);
}
/**
* Catch the case that the chain merkle branch size in the coinbase transaction
* does not match the size of the merkle brach in the AuxPoW header.
*/
@Test
public void shouldRejectIfCoinbaseMerkleBranchSizeMismatch() throws Exception {
final byte[] auxpowAsBytes = getBytes(getClass().getResourceAsStream("auxpow_header.bin"));
final AuxPoW auxpow = new AuxPoW(params, auxpowAsBytes, (ChildMessage) null, params.getDefaultSerializer());
// This will also break the difficulty check, but as that doesn't occur
// until the end, we can get away with it.
final TransactionInput in = auxpow.getCoinbase().getInput(0);
in.getScriptBytes()[40] = 3; // Break the merkle branch length
updateMerkleRootToMatchCoinbase(auxpow);
expectedEx.expect(org.bitcoinj.core.VerificationException.class);
expectedEx.expectMessage("Aux POW merkle branch size does not match parent coinbase");
auxpow.checkProofOfWork(new Sha256Hash("0c836b86991631d34a8a68054e2f62db919b39d1ee43c27ab3344d6aa82fa609"),
Utils.decodeCompactBits(0x1b06f8f0), true);
}
/**
* In order to ensure that the same work is not submitted more than once,
* confirm that the merkle branch index is correct for our chain ID.
*/
@Test
public void shouldRejectIfNonceIncorrect() throws Exception {
final byte[] auxpowAsBytes = getBytes(getClass().getResourceAsStream("auxpow_header.bin"));
final AuxPoW auxpow = new AuxPoW(params, auxpowAsBytes, (ChildMessage) null, params.getDefaultSerializer());
// This will also break the difficulty check, but as that doesn't occur
// until the end, we can get away with it.
final TransactionInput in = auxpow.getCoinbase().getInput(0);
in.getScriptBytes()[44] = (byte) 0xff; // Break the nonce value
updateMerkleRootToMatchCoinbase(auxpow);
expectedEx.expect(org.bitcoinj.core.VerificationException.class);
expectedEx.expectMessage("Aux POW wrong index");
auxpow.checkProofOfWork(new Sha256Hash("0c836b86991631d34a8a68054e2f62db919b39d1ee43c27ab3344d6aa82fa609"),
Utils.decodeCompactBits(0x1b06f8f0), true);
}
/**
* Having validated the AuxPoW header, the last check is that the block hash
* meets the target difficulty.
*/
@Test
public void shouldRejectHashAboveTarget() throws Exception {
final byte[] auxpowAsBytes = getBytes(getClass().getResourceAsStream("auxpow_header.bin"));
final AuxPoW auxpow = new AuxPoW(params, auxpowAsBytes, (ChildMessage) null, params.getDefaultSerializer());
expectedEx.expect(org.bitcoinj.core.VerificationException.class);
expectedEx.expectMessage("Hash is higher than target: a22a9b01671d639fa6389f62ecf8ce69204c8ed41d5f1a745e0c5ba7116d5b4c vs 0");
auxpow.checkProofOfWork(new Sha256Hash("0c836b86991631d34a8a68054e2f62db919b39d1ee43c27ab3344d6aa82fa609"),
Utils.decodeCompactBits(0x00), true);
}
/**
* Fix up the merkle root of the parent block header to match the
* coinbase transaction.
*/
private void updateMerkleRootToMatchCoinbase(final AuxPoW auxpow) {
final Transaction coinbase = auxpow.getCoinbase();
final Sha256Hash revisedCoinbaseHash = coinbase.getHash();
// The coinbase hash is the single leaf node in the merkle tree,
// so to get the root we need to hash it with itself.
// Note that bytes are reversed for hashing
final byte[] revisedMerkleRootBytes = doubleDigestTwoBuffers(
reverseBytes(revisedCoinbaseHash.getBytes()), 0, 32,
reverseBytes(revisedCoinbaseHash.getBytes()), 0, 32);
final Sha256Hash revisedMerkleRoot = new Sha256Hash(reverseBytes(revisedMerkleRootBytes));
auxpow.getParentBlockHeader().setMerkleRoot(revisedMerkleRoot);
auxpow.setCoinbaseBranch(new MerkleBranch(params, auxpow,
Collections.singletonList(revisedCoinbaseHash), MERKLE_ROOT_COINBASE_INDEX));
}
}