diff --git a/rskj-core/src/main/java/co/rsk/mine/MinerServer.java b/rskj-core/src/main/java/co/rsk/mine/MinerServer.java index eaa1bdf4668..fc1ec9ef0ef 100644 --- a/rskj-core/src/main/java/co/rsk/mine/MinerServer.java +++ b/rskj-core/src/main/java/co/rsk/mine/MinerServer.java @@ -18,10 +18,13 @@ package co.rsk.mine; +import co.rsk.bitcoinj.core.BtcBlock; +import co.rsk.bitcoinj.core.BtcTransaction; import co.rsk.core.RskAddress; import org.ethereum.core.Block; import javax.annotation.Nonnull; +import java.util.List; import java.util.Optional; @@ -33,7 +36,22 @@ public interface MinerServer { boolean isRunning(); - SubmitBlockResult submitBitcoinBlock(String blockHashForMergedMining, co.rsk.bitcoinj.core.BtcBlock bitcoinMergedMiningBlock); + SubmitBlockResult submitBitcoinBlockPartialMerkle( + String blockHashForMergedMining, + BtcBlock blockWithOnlyHeader, + BtcTransaction coinbase, + List merkleHashes, + int blockTxnCount + ); + + SubmitBlockResult submitBitcoinBlockTransactions( + String blockHashForMergedMining, + BtcBlock blockWithOnlyHeader, + BtcTransaction coinbase, + List txHashes + ); + + SubmitBlockResult submitBitcoinBlock(String blockHashForMergedMining, BtcBlock bitcoinMergedMiningBlock); boolean generateFallbackBlock(); diff --git a/rskj-core/src/main/java/co/rsk/mine/MinerServerImpl.java b/rskj-core/src/main/java/co/rsk/mine/MinerServerImpl.java index 634454ed73d..04c9a4ec20c 100644 --- a/rskj-core/src/main/java/co/rsk/mine/MinerServerImpl.java +++ b/rskj-core/src/main/java/co/rsk/mine/MinerServerImpl.java @@ -18,6 +18,7 @@ package co.rsk.mine; +import co.rsk.bitcoinj.core.*; import co.rsk.config.MiningConfig; import co.rsk.config.RskMiningConstants; import co.rsk.config.RskSystemProperties; @@ -42,6 +43,7 @@ import org.slf4j.LoggerFactory; import org.spongycastle.crypto.digests.SHA256Digest; import org.spongycastle.util.Arrays; +import org.spongycastle.util.encoders.Hex; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @@ -55,10 +57,12 @@ import java.math.BigInteger; import java.util.*; import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; /** * The MinerServer provides support to components that perform the actual mining. * It builds blocks to mine and publishes blocks once a valid nonce was found by the miner. + * * @author Oscar Guindzberg */ @@ -146,7 +150,7 @@ public MinerServerImpl( secsBetweenFallbackMinedBlocks = config.getAverageFallbackMiningTime(); // default - if (secsBetweenFallbackMinedBlocks==0) { + if (secsBetweenFallbackMinedBlocks == 0) { secsBetweenFallbackMinedBlocks = (config.getBlockchainConfig().getCommonConstants().getDurationLimit()); } autoSwitchBetweenNormalAndFallbackMining = !config.getBlockchainConfig().getCommonConstants().getFallbackMiningDifficulty().equals(BlockDifficulty.ZERO); @@ -158,8 +162,7 @@ public void setSecsBetweenFallbackMinedBlocks(int m) { } private LinkedHashMap createNewBlocksWaitingList() { - return new LinkedHashMap(CACHE_SIZE) - { + return new LinkedHashMap(CACHE_SIZE) { @Override protected boolean removeEldestEntry(Map.Entry eldest) { return size() > CACHE_SIZE; @@ -184,15 +187,14 @@ public void setFallbackMiningState() { fallbackMiningTimer = new Timer("Private mining timer"); } if (!fallbackMiningScheduled) { - fallbackMiningTimer.schedule(new FallbackMiningTask(), 1000,1000 ); + fallbackMiningTimer.schedule(new FallbackMiningTask(), 1000, 1000); fallbackMiningScheduled = true; } // Because the Refresh occurs only once every minute, // we need to create at least one first block to mine Block bestBlock = blockchain.getBestBlock(); buildBlockToMine(bestBlock, false); - } - else { + } else { if (fallbackMiningTimer != null) { fallbackMiningTimer.cancel(); fallbackMiningTimer = null; @@ -317,7 +319,7 @@ public boolean generateFallbackBlock() { } if (!isEvenBlockNumber && privKey1 == null) { - return false; + return false; } if (isEvenBlockNumber && privKey0 == null) { @@ -338,7 +340,7 @@ public boolean generateFallbackBlock() { BlockHeader newHeader = newBlock.getHeader(); newHeader.setTimestamp(this.getCurrentTimeInSeconds()); - Block parentBlock =blockchain.getBlockByHash(newHeader.getParentHash().getBytes()); + Block parentBlock = blockchain.getBlockByHash(newHeader.getParentHash().getBytes()); newHeader.setDifficulty( difficultyCalculator.calcDifficulty(newHeader, parentBlock.getHeader())); @@ -379,16 +381,53 @@ private byte[] fallbackSign(byte[] hash, ECKey privKey) { } @Override - public SubmitBlockResult submitBitcoinBlock(String blockHashForMergedMining, co.rsk.bitcoinj.core.BtcBlock bitcoinMergedMiningBlock) { - return submitBitcoinBlock(blockHashForMergedMining, bitcoinMergedMiningBlock, true); + public SubmitBlockResult submitBitcoinBlockPartialMerkle( + String blockHashForMergedMining, + BtcBlock blockWithHeaderOnly, + BtcTransaction coinbase, + List merkleHashes, + int blockTxnCount) { + logger.debug("Received merkle solution with hash {} for merged mining", blockHashForMergedMining); + + PartialMerkleTree bitcoinMergedMiningMerkleBranch = getBitcoinMergedMerkleBranchForCoinbase(blockWithHeaderOnly.getParams(), merkleHashes, blockTxnCount); + + return processSolution(blockHashForMergedMining, blockWithHeaderOnly, coinbase, bitcoinMergedMiningMerkleBranch, true); + } + + @Override + public SubmitBlockResult submitBitcoinBlockTransactions( + String blockHashForMergedMining, + BtcBlock blockWithHeaderOnly, + BtcTransaction coinbase, + List txHashes) { + logger.debug("Received tx solution with hash {} for merged mining", blockHashForMergedMining); + + PartialMerkleTree bitcoinMergedMiningMerkleBranch = getBitcoinMergedMerkleBranch(blockWithHeaderOnly.getParams(), txHashes); + + return processSolution(blockHashForMergedMining, blockWithHeaderOnly, coinbase, bitcoinMergedMiningMerkleBranch, true); } + @Override + public SubmitBlockResult submitBitcoinBlock(String blockHashForMergedMining, BtcBlock bitcoinMergedMiningBlock) { + return submitBitcoinBlock(blockHashForMergedMining, bitcoinMergedMiningBlock, true); + } - public SubmitBlockResult submitBitcoinBlock(String blockHashForMergedMining, co.rsk.bitcoinj.core.BtcBlock bitcoinMergedMiningBlock, boolean lastTag) { + SubmitBlockResult submitBitcoinBlock(String blockHashForMergedMining, BtcBlock bitcoinMergedMiningBlock, boolean lastTag) { logger.debug("Received block with hash {} for merged mining", blockHashForMergedMining); - co.rsk.bitcoinj.core.BtcTransaction bitcoinMergedMiningCoinbaseTransaction = bitcoinMergedMiningBlock.getTransactions().get(0); - co.rsk.bitcoinj.core.PartialMerkleTree bitcoinMergedMiningMerkleBranch = getBitcoinMergedMerkleBranch(bitcoinMergedMiningBlock); + //noinspection ConstantConditions + BtcTransaction coinbase = bitcoinMergedMiningBlock.getTransactions().get(0); + PartialMerkleTree bitcoinMergedMiningMerkleBranch = getBitcoinMergedMerkleBranch(bitcoinMergedMiningBlock); + + return processSolution(blockHashForMergedMining, bitcoinMergedMiningBlock, coinbase, bitcoinMergedMiningMerkleBranch, lastTag); + } + + private SubmitBlockResult processSolution( + String blockHashForMergedMining, + BtcBlock blockWithHeaderOnly, + BtcTransaction coinbase, + PartialMerkleTree bitcoinMergedMiningMerkleBranch, + boolean lastTag) { Block newBlock; Keccak256 key = new Keccak256(TypeConverter.removeZeroX(blockHashForMergedMining)); @@ -417,8 +456,8 @@ public SubmitBlockResult submitBitcoinBlock(String blockHashForMergedMining, co. logger.info("Received block {} {}", newBlock.getNumber(), newBlock.getHash()); - newBlock.setBitcoinMergedMiningHeader(bitcoinMergedMiningBlock.cloneAsHeader().bitcoinSerialize()); - newBlock.setBitcoinMergedMiningCoinbaseTransaction(compressCoinbase(bitcoinMergedMiningCoinbaseTransaction.bitcoinSerialize(), lastTag)); + newBlock.setBitcoinMergedMiningHeader(blockWithHeaderOnly.cloneAsHeader().bitcoinSerialize()); + newBlock.setBitcoinMergedMiningCoinbaseTransaction(compressCoinbase(coinbase.bitcoinSerialize(), lastTag)); newBlock.setBitcoinMergedMiningMerkleProof(bitcoinMergedMiningMerkleBranch.bitcoinSerialize()); newBlock.seal(); @@ -450,12 +489,12 @@ public static byte[] compressCoinbase(byte[] bitcoinMergedMiningCoinbaseTransact return compressCoinbase(bitcoinMergedMiningCoinbaseTransactionSerialized, true); } - public static byte[] compressCoinbase(byte[] bitcoinMergedMiningCoinbaseTransactionSerialized, boolean lastOcurrence) { + public static byte[] compressCoinbase(byte[] bitcoinMergedMiningCoinbaseTransactionSerialized, boolean lastOccurrence) { List coinBaseTransactionSerializedAsList = java.util.Arrays.asList(ArrayUtils.toObject(bitcoinMergedMiningCoinbaseTransactionSerialized)); List tagAsList = java.util.Arrays.asList(ArrayUtils.toObject(RskMiningConstants.RSK_TAG)); int rskTagPosition; - if (lastOcurrence) { + if (lastOccurrence) { rskTagPosition = Collections.lastIndexOfSubList(coinBaseTransactionSerializedAsList, tagAsList); } else { rskTagPosition = Collections.indexOfSubList(coinBaseTransactionSerializedAsList, tagAsList); @@ -477,6 +516,54 @@ public static byte[] compressCoinbase(byte[] bitcoinMergedMiningCoinbaseTransact return Arrays.concatenate(trimmedHashedContent, unHashedContent); } + /** + * getBitcoinMergedMerkleBranch returns the Partial Merkle Branch needed to validate that the coinbase tx + * is part of the Merkle Tree. + * + * @param networkParams bitcoin network params. + * @param merkleStringHashes hashes for the partial merkle tree of the bitcoin block used for merged mining. + * @param blockTxnCount number of transactions in the block. + * @return A Partial Merkle Branch in which you can validate the coinbase tx. + */ + private PartialMerkleTree getBitcoinMergedMerkleBranchForCoinbase( + NetworkParameters networkParams, + List merkleStringHashes, + int blockTxnCount) { + List merkleHashes = merkleStringHashes.stream().map(mk -> Sha256Hash.wrapReversed(Hex.decode(mk))).collect(Collectors.toList()); + int merkleTreeHeight = (int) Math.ceil(Math.log(blockTxnCount) / Math.log(2)); + + // bitlist will always have ones at the beginning because merkle branch is built for coinbase tx + List bitList = new ArrayList<>(); + for (int i = 0; i < merkleHashes.size() + merkleTreeHeight; i++) { + bitList.add(i < merkleHashes.size()); + } + + // bits indicates which nodes are going to be used for building the partial merkle tree + // for more information please refer to {@link co.rsk.bitcoinj.core.PartialMerkleTree#buildFromLeaves } method + byte[] bits = new byte[(bitList.size() + 7) / 8]; + for (int i = 0; i < bitList.size(); i++) { + if (bitList.get(i)) { + Utils.setBitLE(bits, i); + } + } + + return new PartialMerkleTree(networkParams, bits, merkleHashes, blockTxnCount); + } + + /** + * getBitcoinMergedMerkleBranch returns the Partial Merkle Branch needed to validate that the coinbase tx + * is part of the Merkle Tree. + * + * @param networkParams bitcoin network params. + * @param txStringHashes hashes for the txs of the bitcoin block used for merged mining. + * @return A Partial Merkle Branch in which you can validate the coinbase tx. + */ + private PartialMerkleTree getBitcoinMergedMerkleBranch(NetworkParameters networkParams, List txStringHashes) { + List txHashes = txStringHashes.stream().map(Sha256Hash::wrap).collect(Collectors.toList()); + + return buildMerkleBranch(txHashes, networkParams); + } + /** * getBitcoinMergedMerkleBranch returns the Partial Merkle Branch needed to validate that the coinbase tx * is part of the Merkle Tree. @@ -484,22 +571,27 @@ public static byte[] compressCoinbase(byte[] bitcoinMergedMiningCoinbaseTransact * @param bitcoinMergedMiningBlock the bitcoin block that includes all the txs. * @return A Partial Merkle Branch in which you can validate the coinbase tx. */ - public static co.rsk.bitcoinj.core.PartialMerkleTree getBitcoinMergedMerkleBranch(co.rsk.bitcoinj.core.BtcBlock bitcoinMergedMiningBlock) { - List txs = bitcoinMergedMiningBlock.getTransactions(); - List txHashes = new ArrayList<>(txs.size()); - for (co.rsk.bitcoinj.core.BtcTransaction tx : txs) { + public static PartialMerkleTree getBitcoinMergedMerkleBranch(BtcBlock bitcoinMergedMiningBlock) { + List txs = bitcoinMergedMiningBlock.getTransactions(); + List txHashes = new ArrayList<>(txs.size()); + for (BtcTransaction tx : txs) { txHashes.add(tx.getHash()); } - /** - * We need to convert the txs to a bitvector to choose which ones - * will be included in the Partial Merkle Tree. - * - * We need txs.size() / 8 bytes to represent this vector. - * The coinbase tx is the first one of the txs so we set the first bit to 1. + + return buildMerkleBranch(txHashes, bitcoinMergedMiningBlock.getParams()); + } + + private static PartialMerkleTree buildMerkleBranch(List txHashes, NetworkParameters params) { + /* + We need to convert the txs to a bitvector to choose which ones + will be included in the Partial Merkle Tree. + + We need txs.size() / 8 bytes to represent this vector. + The coinbase tx is the first one of the txs so we set the first bit to 1. */ - byte[] bitvector = new byte[(int) Math.ceil(txs.size() / 8.0)]; - co.rsk.bitcoinj.core.Utils.setBitLE(bitvector, 0); - return co.rsk.bitcoinj.core.PartialMerkleTree.buildFromLeaves(bitcoinMergedMiningBlock.getParams(), bitvector, txHashes); + byte[] bitvector = new byte[(txHashes.size() + 7) / 8]; + Utils.setBitLE(bitvector, 0); + return PartialMerkleTree.buildFromLeaves(params, bitvector, txHashes); } @Override @@ -519,7 +611,7 @@ public MinerWork getWork() { if (work == null) { return null; } - + if (work.getNotify()) { /** * Set currentWork.notify to false for the next time this function is called. @@ -529,11 +621,11 @@ public MinerWork getWork() { * currentWork, regardless of what it is. */ synchronized (lock) { - if (currentWork != work || currentWork == null) { + if (currentWork != work || currentWork == null) { return currentWork; } currentWork = new MinerWork(currentWork.getBlockHashForMergedMining(), currentWork.getTarget(), - currentWork.getFeesPaidToMiner(),false, currentWork.getParentBlockHash()); + currentWork.getFeesPaidToMiner(), false, currentWork.getParentBlockHash()); } } return work; @@ -563,7 +655,7 @@ public void setExtraData(byte[] extraData) { /** * buildBlockToMine creates a block to mine based on the given block as parent. * - * @param newBlockParent the new block parent. + * @param newBlockParent the new block parent. * @param createCompetitiveBlock used for testing. */ @Override @@ -698,16 +790,16 @@ public void run() { long curtimestampSeconds = getCurrentTimeInSeconds(); - if (curtimestampSeconds > bestBlock.getTimestamp() + secsBetweenFallbackMinedBlocks) - { - generateFallbackBlock(); - } + if (curtimestampSeconds > bestBlock.getTimestamp() + secsBetweenFallbackMinedBlocks) { + generateFallbackBlock(); + } } catch (Throwable th) { logger.error("Unexpected error: {}", th); panicProcessor.panic("mserror", th.getMessage()); } } } + /** * RefreshBlocks rebuilds the block to mine. */ diff --git a/rskj-core/src/main/java/co/rsk/mine/MinerUtils.java b/rskj-core/src/main/java/co/rsk/mine/MinerUtils.java index 12111935ba3..5819e49d22b 100644 --- a/rskj-core/src/main/java/co/rsk/mine/MinerUtils.java +++ b/rskj-core/src/main/java/co/rsk/mine/MinerUtils.java @@ -26,7 +26,6 @@ import co.rsk.core.RskAddress; import co.rsk.crypto.Keccak256; import co.rsk.remasc.RemascTransaction; -import com.google.common.collect.Lists; import org.ethereum.core.PendingState; import org.ethereum.core.Repository; import org.ethereum.core.Transaction; @@ -39,10 +38,7 @@ import java.io.IOException; import java.math.BigInteger; import java.security.SecureRandom; -import java.util.ArrayList; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; +import java.util.*; /** * Created by oscar on 26/09/2016. @@ -127,9 +123,11 @@ public static co.rsk.bitcoinj.core.BtcTransaction getBitcoinMergedMiningCoinbase return coinbaseTransaction; } - public static co.rsk.bitcoinj.core.BtcBlock getBitcoinMergedMiningBlock(co.rsk.bitcoinj.core.NetworkParameters params, - co.rsk.bitcoinj.core.BtcTransaction coinbaseTransaction) { - List transactions = Lists.newArrayList(coinbaseTransaction); + public static co.rsk.bitcoinj.core.BtcBlock getBitcoinMergedMiningBlock(co.rsk.bitcoinj.core.NetworkParameters params, BtcTransaction transaction) { + return getBitcoinMergedMiningBlock(params, Collections.singletonList(transaction)); + } + + public static co.rsk.bitcoinj.core.BtcBlock getBitcoinMergedMiningBlock(co.rsk.bitcoinj.core.NetworkParameters params, List transactions) { co.rsk.bitcoinj.core.Sha256Hash prevBlockHash = co.rsk.bitcoinj.core.Sha256Hash.ZERO_HASH; long time = System.currentTimeMillis() / 1000; long difficultyTarget = co.rsk.bitcoinj.core.Utils.encodeCompactBits(params.getMaxTarget()); diff --git a/rskj-core/src/main/java/co/rsk/rpc/Web3RskImpl.java b/rskj-core/src/main/java/co/rsk/rpc/Web3RskImpl.java index b3015ab684e..2ed01cdba33 100644 --- a/rskj-core/src/main/java/co/rsk/rpc/Web3RskImpl.java +++ b/rskj-core/src/main/java/co/rsk/rpc/Web3RskImpl.java @@ -18,6 +18,11 @@ package co.rsk.rpc; +import co.rsk.bitcoinj.core.BtcBlock; +import co.rsk.bitcoinj.core.BtcTransaction; +import co.rsk.bitcoinj.core.Context; +import co.rsk.bitcoinj.core.NetworkParameters; +import co.rsk.bitcoinj.params.RegTestParams; import co.rsk.config.RskMiningConstants; import co.rsk.config.RskSystemProperties; import co.rsk.core.NetworkStateExporter; @@ -48,12 +53,14 @@ import java.io.FileWriter; import java.io.IOException; import java.io.PrintWriter; -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; +import java.util.*; /** - * Created by adrian.eidelman on 3/11/2016. + * Handles requests for work and block submission. + * Full responsibility for processing the request is delegated to MinerServer. + * + * @author Adrian Eidelman + * @author Martin Medina */ public class Web3RskImpl extends Web3Impl { private static final Logger logger = LoggerFactory.getLogger("web3"); @@ -85,42 +92,77 @@ public Web3RskImpl(Ethereum eth, } public MinerWork mnr_getWork() { - if (logger.isDebugEnabled()) { - logger.debug("mnr_getWork()"); - } + logger.debug("mnr_getWork()"); + return minerServer.getWork(); } public SubmittedBlockInfo mnr_submitBitcoinBlock(String bitcoinBlockHex) { - if (logger.isDebugEnabled()) { - logger.debug("mnr_submitBitcoinBlock(): {}", bitcoinBlockHex.length()); - } - - co.rsk.bitcoinj.core.NetworkParameters params = co.rsk.bitcoinj.params.RegTestParams.get(); - new co.rsk.bitcoinj.core.Context(params); - byte[] bitcoinBlockByteArray = Hex.decode(bitcoinBlockHex); - co.rsk.bitcoinj.core.BtcBlock bitcoinBlock = params.getDefaultSerializer().makeBlock(bitcoinBlockByteArray); - co.rsk.bitcoinj.core.BtcTransaction coinbase = bitcoinBlock.getTransactions().get(0); - byte[] coinbaseAsByteArray = coinbase.bitcoinSerialize(); - List coinbaseAsByteList = java.util.Arrays.asList(ArrayUtils.toObject(coinbaseAsByteArray)); + logger.debug("mnr_submitBitcoinBlock(): {}", bitcoinBlockHex.length()); - List rskTagAsByteList = java.util.Arrays.asList(ArrayUtils.toObject(RskMiningConstants.RSK_TAG)); + NetworkParameters params = RegTestParams.get(); + new Context(params); - int rskTagPosition = Collections.lastIndexOfSubList(coinbaseAsByteList, rskTagAsByteList); - byte[] blockHashForMergedMiningArray = new byte[Keccak256Helper.Size.S256.getValue()/8]; - System.arraycopy(coinbaseAsByteArray, rskTagPosition+ RskMiningConstants.RSK_TAG.length, blockHashForMergedMiningArray, 0, blockHashForMergedMiningArray.length); - String blockHashForMergedMining = TypeConverter.toJsonHex(blockHashForMergedMiningArray); + BtcBlock bitcoinBlock = getBtcBlock(bitcoinBlockHex, params); + BtcTransaction coinbase = bitcoinBlock.getTransactions().get(0); + + String blockHashForMergedMining = extractBlockHashForMergedMining(coinbase); SubmitBlockResult result = minerServer.submitBitcoinBlock(blockHashForMergedMining, bitcoinBlock); - if("OK".equals(result.getStatus())) { - return result.getBlockInfo(); - } else { - throw new JsonRpcSubmitBlockException(result.getMessage()); - } + return parseResultAndReturn(result); + } + + public SubmittedBlockInfo mnr_submitBitcoinBlockPartialMerkle( + String blockHashHex, + String blockHeaderHex, + String coinbaseHex, + String merkleHashesHex, + String blockTxnCountHex + ) { + logger.debug("mnr_submitBitcoinBlockPartialMerkle(): {}, {}, {}, {}, {}", blockHashHex, blockHeaderHex, coinbaseHex, merkleHashesHex, blockTxnCountHex); + + NetworkParameters params = RegTestParams.get(); + new Context(params); + + BtcBlock bitcoinBlockWithHeaderOnly = getBtcBlock(blockHeaderHex, params); + BtcTransaction coinbase = new BtcTransaction(params, Hex.decode(coinbaseHex)); + + String blockHashForMergedMining = extractBlockHashForMergedMining(coinbase); + + List merkleHashes = parseHashes(merkleHashesHex); + + int txnCount = Integer.parseInt(blockTxnCountHex, 16); + + SubmitBlockResult result = minerServer.submitBitcoinBlockPartialMerkle(blockHashForMergedMining, bitcoinBlockWithHeaderOnly, coinbase, merkleHashes, txnCount); + + return parseResultAndReturn(result); + } + + public SubmittedBlockInfo mnr_submitBitcoinBlockTransactions( + String blockHashHex, + String blockHeaderHex, + String coinbaseHex, + String txnHashesHex + ) { + logger.debug("mnr_submitBitcoinBlockTransactions(): {}, {}, {}, {}", blockHashHex, blockHeaderHex, coinbaseHex, txnHashesHex); + + NetworkParameters params = RegTestParams.get(); + new Context(params); + + BtcBlock bitcoinBlockWithHeaderOnly = getBtcBlock(blockHeaderHex, params); + BtcTransaction coinbase = new BtcTransaction(params, Hex.decode(coinbaseHex)); + + String blockHashForMergedMining = extractBlockHashForMergedMining(coinbase); + + List txnHashes = parseHashes(txnHashesHex); + + SubmitBlockResult result = minerServer.submitBitcoinBlockTransactions(blockHashForMergedMining, bitcoinBlockWithHeaderOnly, coinbase, txnHashes); + + return parseResultAndReturn(result); } - public void ext_dumpState() { + public void ext_dumpState() { Block bestBlcock = blockStore.getBestBlock(); logger.info("Dumping state for block hash {}, block number {}", bestBlcock.getHash(), bestBlcock.getNumber()); networkStateExporter.exportStatus(System.getProperty("user.dir") + "/" + "rskdump.json"); @@ -128,12 +170,13 @@ public void ext_dumpState() { /** * Export the blockchain tree as a tgf file to user.dir/rskblockchain.tgf + * * @param numberOfBlocks Number of block heights to include. Eg if best block is block 2300 and numberOfBlocks is 10, the graph will include blocks in heights 2290 to 2300. - * @param includeUncles Whether to show uncle links (recommended value is false) + * @param includeUncles Whether to show uncle links (recommended value is false) */ - public void ext_dumpBlockchain(long numberOfBlocks, boolean includeUncles) { + public void ext_dumpBlockchain(long numberOfBlocks, boolean includeUncles) { Block bestBlock = blockStore.getBestBlock(); - logger.info("Dumping blockchain starting on block number {}, to best block number {}", bestBlock.getNumber()-numberOfBlocks, bestBlock.getNumber()); + logger.info("Dumping blockchain starting on block number {}, to best block number {}", bestBlock.getNumber() - numberOfBlocks, bestBlock.getNumber()); PrintWriter writer = null; try { File graphFile = new File(System.getProperty("user.dir") + "/" + "rskblockchain.tgf"); @@ -148,7 +191,7 @@ public void ext_dumpBlockchain(long numberOfBlocks, boolean includeUncles) { result.addAll(blockStore.getChainBlocksByNumber(i)); } for (Block block : result) { - writer.println(toSmallHash(block.getHash().getBytes()) + " " + block.getNumber()+"-"+toSmallHash(block.getHash().getBytes())); + writer.println(toSmallHash(block.getHash().getBytes()) + " " + block.getNumber() + "-" + toSmallHash(block.getHash().getBytes())); } writer.println("#"); for (Block block : result) { @@ -162,16 +205,46 @@ public void ext_dumpBlockchain(long numberOfBlocks, boolean includeUncles) { } catch (IOException e) { logger.error("Could nos save node graph to file", e); } finally { - if (writer!=null) { + if (writer != null) { try { writer.close(); - } catch (Exception e) {} + } catch (Exception e) { + } } } } private String toSmallHash(byte[] input) { - return Hex.toHexString(input).substring(56,64); + return Hex.toHexString(input).substring(56, 64); + } + + private BtcBlock getBtcBlock(String blockHeaderHex, NetworkParameters params) { + byte[] bitcoinBlockByteArray = Hex.decode(blockHeaderHex); + return params.getDefaultSerializer().makeBlock(bitcoinBlockByteArray); + } + + private String extractBlockHashForMergedMining(BtcTransaction coinbase) { + byte[] coinbaseAsByteArray = coinbase.bitcoinSerialize(); + List coinbaseAsByteList = Arrays.asList(ArrayUtils.toObject(coinbaseAsByteArray)); + + List rskTagAsByteList = Arrays.asList(ArrayUtils.toObject(RskMiningConstants.RSK_TAG)); + + int rskTagPosition = Collections.lastIndexOfSubList(coinbaseAsByteList, rskTagAsByteList); + byte[] blockHashForMergedMiningArray = new byte[Keccak256Helper.Size.S256.getValue() / 8]; + System.arraycopy(coinbaseAsByteArray, rskTagPosition + RskMiningConstants.RSK_TAG.length, blockHashForMergedMiningArray, 0, blockHashForMergedMiningArray.length); + return TypeConverter.toJsonHex(blockHashForMergedMiningArray); + } + + private List parseHashes(String txnHashesHex) { + String[] split = txnHashesHex.split("\\s+"); + return Arrays.asList(split); } + private SubmittedBlockInfo parseResultAndReturn(SubmitBlockResult result) { + if ("OK".equals(result.getStatus())) { + return result.getBlockInfo(); + } else { + throw new JsonRpcSubmitBlockException(result.getMessage()); + } + } } diff --git a/rskj-core/src/test/java/co/rsk/mine/MinerServerTest.java b/rskj-core/src/test/java/co/rsk/mine/MinerServerTest.java index c81e42964ce..3d5c82e71b3 100644 --- a/rskj-core/src/test/java/co/rsk/mine/MinerServerTest.java +++ b/rskj-core/src/test/java/co/rsk/mine/MinerServerTest.java @@ -19,8 +19,8 @@ package co.rsk.mine; import co.rsk.TestHelpers.Tx; -import co.rsk.bitcoinj.core.NetworkParameters; -import co.rsk.bitcoinj.core.VerificationException; +import co.rsk.bitcoinj.core.*; +import co.rsk.bitcoinj.params.RegTestParams; import co.rsk.config.ConfigUtils; import co.rsk.config.RskSystemProperties; import co.rsk.core.Coin; @@ -45,9 +45,8 @@ import java.math.BigInteger; import java.nio.ByteBuffer; -import java.util.ArrayList; +import java.util.*; import java.util.Arrays; -import java.util.List; import static org.hamcrest.CoreMatchers.instanceOf; import static org.junit.Assert.*; @@ -57,7 +56,7 @@ */ public class MinerServerTest { private static final RskSystemProperties config = new RskSystemProperties(); - public static final DifficultyCalculator DIFFICULTY_CALCULATOR = new DifficultyCalculator(config); + private static final DifficultyCalculator DIFFICULTY_CALCULATOR = new DifficultyCalculator(config); private BlockChainImpl blockchain; @@ -77,9 +76,7 @@ public void buildBlockToMineCheckThatLastTransactionIsForREMASC() { Transaction tx1 = Tx.create(config, 0, 21000, 100, 0, 0, 0); byte[] s1 = new byte[32]; - byte[] s2 = new byte[32]; s1[0] = 0; - s2[0] = 1; Mockito.when(tx1.getHash()).thenReturn(new Keccak256(s1)); Mockito.when(tx1.getEncoded()).thenReturn(new byte[32]); @@ -88,7 +85,7 @@ public void buildBlockToMineCheckThatLastTransactionIsForREMASC() { Mockito.when(repository.getBalance(tx1.getSender())).thenReturn(Coin.valueOf(4200000L)); Mockito.when(repository.getBalance(RemascTransaction.REMASC_ADDRESS)).thenReturn(Coin.valueOf(4200000L)); - List txs = new ArrayList<>(Arrays.asList(tx1)); + List txs = new ArrayList<>(Collections.singletonList(tx1)); PendingState localPendingState = Mockito.mock(PendingState.class); Mockito.when(localPendingState.getPendingTransactions()).thenReturn(txs); @@ -127,8 +124,6 @@ public void buildBlockToMineCheckThatLastTransactionIsForREMASC() { assertThat(remascTransaction, instanceOf(RemascTransaction.class)); } - - @Test public void submitBitcoinBlockTwoTags() { EthereumImpl ethereumImpl = Mockito.mock(EthereumImpl.class); @@ -169,7 +164,7 @@ public void submitBitcoinBlockTwoTags() { MinerWork work2 = minerServer.getWork(); // only the tag is used Assert.assertNotEquals(work2.getBlockHashForMergedMining(),work.getBlockHashForMergedMining()); - co.rsk.bitcoinj.core.BtcBlock bitcoinMergedMiningBlock = getMergedMiningBlockWithTwoTags(work,work2); + BtcBlock bitcoinMergedMiningBlock = getMergedMiningBlockWithTwoTags(work,work2); findNonce(work, bitcoinMergedMiningBlock); SubmitBlockResult result; @@ -191,6 +186,7 @@ public void submitBitcoinBlockTwoTags() { minerServer.stop(); } } + @Test public void submitBitcoinBlock() { EthereumImpl ethereumImpl = Mockito.mock(EthereumImpl.class); @@ -219,27 +215,240 @@ public void submitBitcoinBlock() { ConfigUtils.getDefaultMiningConfig() ); try { - minerServer.start(); - MinerWork work = minerServer.getWork(); + minerServer.start(); + MinerWork work = minerServer.getWork(); - co.rsk.bitcoinj.core.BtcBlock bitcoinMergedMiningBlock = getMergedMiningBlock(work); + BtcBlock bitcoinMergedMiningBlock = getMergedMiningBlockWithOnlyCoinbase(work); - findNonce(work, bitcoinMergedMiningBlock); + findNonce(work, bitcoinMergedMiningBlock); - SubmitBlockResult result = minerServer.submitBitcoinBlock(work.getBlockHashForMergedMining(), bitcoinMergedMiningBlock); + SubmitBlockResult result = minerServer.submitBitcoinBlock(work.getBlockHashForMergedMining(), bitcoinMergedMiningBlock); - Assert.assertEquals("OK", result.getStatus()); - Assert.assertNotNull(result.getBlockInfo()); - Assert.assertEquals("0x1", result.getBlockInfo().getBlockIncludedHeight()); - Assert.assertEquals("0x494d504f525445445f42455354", result.getBlockInfo().getBlockImportedResult()); + Assert.assertEquals("OK", result.getStatus()); + Assert.assertNotNull(result.getBlockInfo()); + Assert.assertEquals("0x1", result.getBlockInfo().getBlockIncludedHeight()); + Assert.assertEquals("0x494d504f525445445f42455354", result.getBlockInfo().getBlockImportedResult()); - Mockito.verify(ethereumImpl, Mockito.times(1)).addNewMinedBlock(Mockito.any()); + Mockito.verify(ethereumImpl, Mockito.times(1)).addNewMinedBlock(Mockito.any()); + } finally { + minerServer.stop(); + } + } + + @Test + public void submitBitcoinBlockPartialMerkleWhenBlockIsEmpty() { + EthereumImpl ethereumImpl = Mockito.mock(EthereumImpl.class); + Mockito.when(ethereumImpl.addNewMinedBlock(Mockito.any())).thenReturn(ImportResult.IMPORTED_BEST); + + BlockUnclesValidationRule unclesValidationRule = Mockito.mock(BlockUnclesValidationRule.class); + Mockito.when(unclesValidationRule.isValid(Mockito.any())).thenReturn(true); + MinerServer minerServer = new MinerServerImpl( + config, + ethereumImpl, + blockchain, + null, + DIFFICULTY_CALCULATOR, + new ProofOfWorkRule(config).setFallbackMiningEnabled(false), + new BlockToMineBuilder( + ConfigUtils.getDefaultMiningConfig(), + blockchain.getRepository(), + blockchain.getBlockStore(), + blockchain.getPendingState(), + DIFFICULTY_CALCULATOR, + new GasLimitCalculator(config), + unclesValidationRule, + config, + null + ), + ConfigUtils.getDefaultMiningConfig() + ); + try { + minerServer.start(); + MinerWork work = minerServer.getWork(); + + BtcBlock bitcoinMergedMiningBlock = getMergedMiningBlockWithOnlyCoinbase(work); + + findNonce(work, bitcoinMergedMiningBlock); + + //noinspection ConstantConditions + BtcTransaction coinbase = bitcoinMergedMiningBlock.getTransactions().get(0); + List coinbaseReversedHash = Collections.singletonList(Sha256Hash.wrap(coinbase.getHash().getReversedBytes()).toString()); + SubmitBlockResult result = minerServer.submitBitcoinBlockPartialMerkle(work.getBlockHashForMergedMining(), bitcoinMergedMiningBlock, coinbase, coinbaseReversedHash, 1); + + Assert.assertEquals("OK", result.getStatus()); + Assert.assertNotNull(result.getBlockInfo()); + Assert.assertEquals("0x1", result.getBlockInfo().getBlockIncludedHeight()); + Assert.assertEquals("0x494d504f525445445f42455354", result.getBlockInfo().getBlockImportedResult()); + + Mockito.verify(ethereumImpl, Mockito.times(1)).addNewMinedBlock(Mockito.any()); + } finally { + minerServer.stop(); + } + } + + @Test + public void submitBitcoinBlockPartialMerkleWhenBlockHasTransactions() { + EthereumImpl ethereumImpl = Mockito.mock(EthereumImpl.class); + Mockito.when(ethereumImpl.addNewMinedBlock(Mockito.any())).thenReturn(ImportResult.IMPORTED_BEST); + + BlockUnclesValidationRule unclesValidationRule = Mockito.mock(BlockUnclesValidationRule.class); + Mockito.when(unclesValidationRule.isValid(Mockito.any())).thenReturn(true); + MinerServer minerServer = new MinerServerImpl( + config, + ethereumImpl, + blockchain, + null, + DIFFICULTY_CALCULATOR, + new ProofOfWorkRule(config).setFallbackMiningEnabled(false), + new BlockToMineBuilder( + ConfigUtils.getDefaultMiningConfig(), + blockchain.getRepository(), + blockchain.getBlockStore(), + blockchain.getPendingState(), + DIFFICULTY_CALCULATOR, + new GasLimitCalculator(config), + unclesValidationRule, + config, + null + ), + ConfigUtils.getDefaultMiningConfig() + ); + try { + minerServer.start(); + MinerWork work = minerServer.getWork(); + + BtcTransaction otherTx = Mockito.mock(BtcTransaction.class); + Sha256Hash otherTxHash = Sha256Hash.wrap("aaaabbbbccccddddaaaabbbbccccddddaaaabbbbccccddddaaaabbbbccccdddd"); + Mockito.when(otherTx.getHash()).thenReturn(otherTxHash); + Mockito.when(otherTx.getHashAsString()).thenReturn(otherTxHash.toString()); + + BtcBlock bitcoinMergedMiningBlock = getMergedMiningBlock(work, Collections.singletonList(otherTx)); + + findNonce(work, bitcoinMergedMiningBlock); + + //noinspection ConstantConditions + BtcTransaction coinbase = bitcoinMergedMiningBlock.getTransactions().get(0); + String coinbaseReversedHash = Sha256Hash.wrap(coinbase.getHash().getReversedBytes()).toString(); + String otherTxHashReversed = Sha256Hash.wrap(otherTxHash.getReversedBytes()).toString(); + List merkleHashes = Arrays.asList(coinbaseReversedHash, otherTxHashReversed); + SubmitBlockResult result = minerServer.submitBitcoinBlockPartialMerkle(work.getBlockHashForMergedMining(), bitcoinMergedMiningBlock, coinbase, merkleHashes, 2); + + Assert.assertEquals("OK", result.getStatus()); + Assert.assertNotNull(result.getBlockInfo()); + Assert.assertEquals("0x1", result.getBlockInfo().getBlockIncludedHeight()); + Assert.assertEquals("0x494d504f525445445f42455354", result.getBlockInfo().getBlockImportedResult()); + + Mockito.verify(ethereumImpl, Mockito.times(1)).addNewMinedBlock(Mockito.any()); + } finally { + minerServer.stop(); + } + } + + @Test + public void submitBitcoinBlockTransactionsWhenBlockIsEmpty() { + EthereumImpl ethereumImpl = Mockito.mock(EthereumImpl.class); + Mockito.when(ethereumImpl.addNewMinedBlock(Mockito.any())).thenReturn(ImportResult.IMPORTED_BEST); + + BlockUnclesValidationRule unclesValidationRule = Mockito.mock(BlockUnclesValidationRule.class); + Mockito.when(unclesValidationRule.isValid(Mockito.any())).thenReturn(true); + MinerServer minerServer = new MinerServerImpl( + config, + ethereumImpl, + blockchain, + null, + DIFFICULTY_CALCULATOR, + new ProofOfWorkRule(config).setFallbackMiningEnabled(false), + new BlockToMineBuilder( + ConfigUtils.getDefaultMiningConfig(), + blockchain.getRepository(), + blockchain.getBlockStore(), + blockchain.getPendingState(), + DIFFICULTY_CALCULATOR, + new GasLimitCalculator(config), + unclesValidationRule, + config, + null + ), + ConfigUtils.getDefaultMiningConfig() + ); + try { + minerServer.start(); + MinerWork work = minerServer.getWork(); + + BtcBlock bitcoinMergedMiningBlock = getMergedMiningBlockWithOnlyCoinbase(work); + + findNonce(work, bitcoinMergedMiningBlock); + + //noinspection ConstantConditions + BtcTransaction coinbase = bitcoinMergedMiningBlock.getTransactions().get(0); + SubmitBlockResult result = minerServer.submitBitcoinBlockTransactions(work.getBlockHashForMergedMining(), bitcoinMergedMiningBlock, coinbase, Collections.singletonList(coinbase.getHashAsString())); + + Assert.assertEquals("OK", result.getStatus()); + Assert.assertNotNull(result.getBlockInfo()); + Assert.assertEquals("0x1", result.getBlockInfo().getBlockIncludedHeight()); + Assert.assertEquals("0x494d504f525445445f42455354", result.getBlockInfo().getBlockImportedResult()); + + Mockito.verify(ethereumImpl, Mockito.times(1)).addNewMinedBlock(Mockito.any()); } finally { minerServer.stop(); } } + @Test + public void submitBitcoinBlockTransactionsWhenBlockHasTransactions() { + EthereumImpl ethereumImpl = Mockito.mock(EthereumImpl.class); + Mockito.when(ethereumImpl.addNewMinedBlock(Mockito.any())).thenReturn(ImportResult.IMPORTED_BEST); + BlockUnclesValidationRule unclesValidationRule = Mockito.mock(BlockUnclesValidationRule.class); + Mockito.when(unclesValidationRule.isValid(Mockito.any())).thenReturn(true); + MinerServer minerServer = new MinerServerImpl( + config, + ethereumImpl, + blockchain, + null, + DIFFICULTY_CALCULATOR, + new ProofOfWorkRule(config).setFallbackMiningEnabled(false), + new BlockToMineBuilder( + ConfigUtils.getDefaultMiningConfig(), + blockchain.getRepository(), + blockchain.getBlockStore(), + blockchain.getPendingState(), + DIFFICULTY_CALCULATOR, + new GasLimitCalculator(config), + unclesValidationRule, + config, + null + ), + ConfigUtils.getDefaultMiningConfig() + ); + try { + minerServer.start(); + MinerWork work = minerServer.getWork(); + + BtcTransaction otherTx = Mockito.mock(BtcTransaction.class); + Sha256Hash otherTxHash = Sha256Hash.wrap("aaaabbbbccccddddaaaabbbbccccddddaaaabbbbccccddddaaaabbbbccccdddd"); + Mockito.when(otherTx.getHash()).thenReturn(otherTxHash); + Mockito.when(otherTx.getHashAsString()).thenReturn(otherTxHash.toString()); + + BtcBlock bitcoinMergedMiningBlock = getMergedMiningBlock(work, Collections.singletonList(otherTx)); + + findNonce(work, bitcoinMergedMiningBlock); + + //noinspection ConstantConditions + BtcTransaction coinbase = bitcoinMergedMiningBlock.getTransactions().get(0); + List txs = Arrays.asList(coinbase.getHashAsString(), otherTxHash.toString()); + SubmitBlockResult result = minerServer.submitBitcoinBlockTransactions(work.getBlockHashForMergedMining(), bitcoinMergedMiningBlock, coinbase, txs); + + Assert.assertEquals("OK", result.getStatus()); + Assert.assertNotNull(result.getBlockInfo()); + Assert.assertEquals("0x1", result.getBlockInfo().getBlockIncludedHeight()); + Assert.assertEquals("0x494d504f525445445f42455354", result.getBlockInfo().getBlockImportedResult()); + + Mockito.verify(ethereumImpl, Mockito.times(1)).addNewMinedBlock(Mockito.any()); + } finally { + minerServer.stop(); + } + } @Test public void workWithNoTransactionsZeroFees() { @@ -453,20 +662,29 @@ public void increaseTimeTwice() { Assert.assertTrue(result <= current + 11); } - private co.rsk.bitcoinj.core.BtcBlock getMergedMiningBlock(MinerWork work) { - NetworkParameters bitcoinNetworkParameters = co.rsk.bitcoinj.params.RegTestParams.get(); - co.rsk.bitcoinj.core.BtcTransaction bitcoinMergedMiningCoinbaseTransaction = MinerUtils.getBitcoinMergedMiningCoinbaseTransaction(bitcoinNetworkParameters, work); - return MinerUtils.getBitcoinMergedMiningBlock(bitcoinNetworkParameters, bitcoinMergedMiningCoinbaseTransaction); + private BtcBlock getMergedMiningBlockWithOnlyCoinbase(MinerWork work) { + return getMergedMiningBlock(work, Collections.emptyList()); + } + + private BtcBlock getMergedMiningBlock(MinerWork work, List txs) { + NetworkParameters bitcoinNetworkParameters = RegTestParams.get(); + BtcTransaction bitcoinMergedMiningCoinbaseTransaction = MinerUtils.getBitcoinMergedMiningCoinbaseTransaction(bitcoinNetworkParameters, work); + + List blockTxs = new ArrayList<>(); + blockTxs.add(bitcoinMergedMiningCoinbaseTransaction); + blockTxs.addAll(txs); + + return MinerUtils.getBitcoinMergedMiningBlock(bitcoinNetworkParameters, blockTxs); } - private co.rsk.bitcoinj.core.BtcBlock getMergedMiningBlockWithTwoTags(MinerWork work,MinerWork work2) { - NetworkParameters bitcoinNetworkParameters = co.rsk.bitcoinj.params.RegTestParams.get(); - co.rsk.bitcoinj.core.BtcTransaction bitcoinMergedMiningCoinbaseTransaction = - MinerUtils.getBitcoinMergedMiningCoinbaseTransactionWithTwoTags(bitcoinNetworkParameters, work,work2); + private BtcBlock getMergedMiningBlockWithTwoTags(MinerWork work, MinerWork work2) { + NetworkParameters bitcoinNetworkParameters = RegTestParams.get(); + BtcTransaction bitcoinMergedMiningCoinbaseTransaction = + MinerUtils.getBitcoinMergedMiningCoinbaseTransactionWithTwoTags(bitcoinNetworkParameters, work, work2); return MinerUtils.getBitcoinMergedMiningBlock(bitcoinNetworkParameters, bitcoinMergedMiningCoinbaseTransaction); } - private void findNonce(MinerWork work, co.rsk.bitcoinj.core.BtcBlock bitcoinMergedMiningBlock) { + private void findNonce(MinerWork work, BtcBlock bitcoinMergedMiningBlock) { BigInteger target = new BigInteger(TypeConverter.stringHexToByteArray(work.getTarget())); while (true) {