/*
* Copyright 2014 http://Bither.net
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.bither.bitherj.core;
import net.bither.bitherj.BitherjSettings;
import net.bither.bitherj.db.AbstractDb;
import net.bither.bitherj.exception.VerificationException;
import net.bither.bitherj.utils.Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
public class BlockChain {
private static BlockChain uniqueInstance = new BlockChain();
private static final Logger log = LoggerFactory.getLogger(BlockChain.class);
protected HashMap<byte[], Block> singleBlocks;
protected Block lastBlock;
protected Block lastOrphanBlock;
BlockChain() {
AbstractDb.blockProvider.cleanOldBlock();
this.singleBlocks = new HashMap<byte[], Block>();
this.lastBlock = AbstractDb.blockProvider.getLastBlock();
this.lastOrphanBlock = AbstractDb.blockProvider.getLastOrphanBlock();
}
public static BlockChain getInstance() {
return uniqueInstance;
}
public void addSPVBlock(Block block) {
// only none block need add spv block
if (this.getBlockCount() == 0) {
block.setMain(true);
this.addBlock(block);
this.lastBlock = block;
}
}
public void addBlocks(List<Block> blocks) {
AbstractDb.blockProvider.addBlocks(blocks);
}
public Block getLastBlock() {
return this.lastBlock;
}
public Block getBlock(byte[] blockHash) {
return AbstractDb.blockProvider.getBlock(blockHash);
}
public int getBlockCount() {
return AbstractDb.blockProvider.getBlockCount();
}
public List<byte[]> getBlockLocatorArray() {
// append 10 most recent block hashes, descending, then continue appending, doubling the step back each time,
// finishing with the genesis block (top, -1, -2, -3, -4, -5, -6, -7, -8, -9, -11, -15, -23, -39, -71, -135, ..., 0)
ArrayList<byte[]> locators = new ArrayList<byte[]>();
int step = 1, start = 0;
Block b = this.lastBlock;
while (b != null && b.getBlockNo() > 0) {
locators.add(b.getBlockHash());
if (++start >= 10) step *= 2;
for (int i = 0; b != null && i < step; i++) {
b = AbstractDb.blockProvider.getMainChainBlock(b.getBlockPrev());
}
}
locators.add(BitherjSettings.GENESIS_BLOCK_HASH);
return locators;
}
public boolean rollbackBlock(int blockNo) {
log.warn("block chain roll back to " + blockNo);
if (blockNo > this.lastBlock.getBlockNo())
return false;
int delta = this.lastBlock.getBlockNo() - blockNo;
if (delta >= BitherjSettings.BLOCK_DIFFICULTY_INTERVAL || delta >= this.getBlockCount())
return false;
List<Block> blocks = AbstractDb.blockProvider.getBlocksFrom(blockNo);
// DDLogWarn(@"roll back block from %d to %d", self.lastBlock.height, blockNo);
for (Block block : blocks) {
AbstractDb.blockProvider.removeBlock(block.getBlockHash());
if (block.isMain()) {
AbstractDb.txProvider.unConfirmTxByBlockNo(block.getBlockNo());
}
}
this.lastBlock = AbstractDb.blockProvider.getLastBlock();
return true;
}
public int relayedBlockHeadersForMainChain(List<Block> blocks) {
if (blocks == null || blocks.size() == 0) {
return 0;
}
ArrayList<Block> blocksToAdd = new ArrayList<Block>();
Block prev = getLastBlock();
if (prev == null) {
log.warn("pre block is null");
return 0;
}
for (int i = 0; i < blocks.size(); i++) {
Block block = blocks.get(i);
if (!Arrays.equals(prev.getBlockHash(), block.getBlockPrev())) {
Block alreadyIn = getBlock(block.getBlockHash());
if (alreadyIn != null) {
log.debug("Block is already in, No." + alreadyIn.getBlockNo());
continue;
} else {
this.singleBlocks.put(block.getBlockHash(), block);
break;
}
}
block.setBlockNo(prev.getBlockNo() + 1);
try {
block.verifyDifficultyFromPreviousBlock(prev);
} catch (Exception e) {
e.printStackTrace();
break;
}
block.setMain(true);
blocksToAdd.add(block);
prev = block;
}
if (blocksToAdd.size() > 0) {
addBlocks(blocksToAdd);
lastBlock = blocksToAdd.get(blocksToAdd.size() - 1);
}
return blocksToAdd.size();
}
/*
* if result is true, means the block is in main chain, if result is false, means the block is single
* or orphan.
* */
public boolean relayedBlock(Block block) throws VerificationException {
Block prev = AbstractDb.blockProvider.getBlock(block.getBlockPrev());
if (prev == null) {
log.debug("prev block is null, prev hash is : " + Utils.hashToString(block.getBlockPrev()));
// DDLogDebug(@"%@:%d relayed orphan block %@, previous %@, last block is %@, height %d", peer.host, peer.port,
// block.blockHash, block.prevBlock, self.lastBlock.blockHash, self.lastBlock.height);
// ignore orphans older than one week ago
// if (block.blockTime - NSTimeIntervalSince1970 < [NSDate timeIntervalSinceReferenceDate] - ONE_WEEK) return;
this.singleBlocks.put(block.getBlockPrev(), block);
return false;
// // call get blocks, unless we already did with the previous block, or we're still downloading the chain
// if (self.lastBlock.height >= peer.lastBlock && ![self.lastOrphan.blockHash isEqual:block.prevBlock]) {
// DDLogDebug(@"%@:%d calling getblocks", peer.host, peer.port);
// [peer sendGetBlocksMessageWithLocators:[self blockLocatorArray] andHashStop:nil];
// }
}
block.setBlockNo(prev.getBlockNo() + 1);
//TODO
// int transitionTime = 0;
// // hit a difficulty transition, find previous transition time
// if ((block.getBlockNo() % BitherjSettings.BLOCK_DIFFICULTY_INTERVAL) == 0) {
// Block b = block;
// for (int i = 0; b != null && i < BitherjSettings.BLOCK_DIFFICULTY_INTERVAL; i++) {
// b = DbHelper.blockProvider.getBlock(b.getBlockPrev());
// }
// transitionTime = b.getBlockTime();
// }
// verify block difficulty
block.verifyDifficultyFromPreviousBlock(prev);
// if (!block.verifyDifficultyFromPreviousBlock(prev)) {
// callback(block, NO);
// return;
// }
boolean result = false;
if (Arrays.equals(block.getBlockPrev(), this.lastBlock.getBlockHash())) {
this.extendMainChain(block);
result = true;
} else if (this.inMainChain(block)) {
result = true;
} else {
if (block.getBlockNo() <= BitherjSettings.BITCOIN_REFERENCE_BLOCK_HEIGHT) {
log.debug("block is too old");
return false;
}
if (block.getBlockNo() <= this.lastBlock.getBlockNo()) {
this.addOrphan(block);
log.debug("block is orphan");
return false;
}
if (block.getBlockNo() > this.lastBlock.getBlockNo()) {
Block b = this.getSameParent(block, this.lastBlock);
this.rollbackBlock(b.getBlockNo());
log.debug("roll back block from" + b.getBlockNo());
}
}
if (!result)
log.debug("block is not in main chain");
return result;
}
public int relayedBlocks(List<Block> blocks) throws VerificationException {
if (blocks == null || blocks.size() == 0) {
return 0;
}
Block prev = null;
Block first = blocks.get(0);
int rollbackBlockNo = 0;
if (Arrays.equals(first.getBlockPrev(), this.getLastBlock().getBlockHash())) {
prev = this.getLastBlock();
} else if (AbstractDb.blockProvider.getMainChainBlock(first.getBlockPrev()) != null) {
prev = this.getSameParent(first, this.getLastBlock());
rollbackBlockNo = prev.getBlockNo();
}
if (prev == null) {
return 0;
}
for (Block block : blocks) {
if (!Arrays.equals(block.getBlockPrev(), prev.getBlockHash())) {
return 0;
}
block.setBlockNo(prev.getBlockNo() + 1);
try {
int transitionTime = 0;
if (block.getBlockNo() % BitherjSettings.BLOCK_DIFFICULTY_INTERVAL == 0) {
// We need to find a block far back in the chain. It's OK that this is expensive because it only occurs every
// two weeks after the initial block chain download.
long now = System.currentTimeMillis();
Block cursor = first;
for (int i = 0; i < BitherjSettings.BLOCK_DIFFICULTY_INTERVAL - block.getBlockNo() + first.getBlockNo(); i++) {
if (cursor == null) {
// This should never happen. If it does, it means we are following an incorrect or busted chain.
throw new VerificationException(
"Difficulty transition point but we did not find a way back to the genesis block.");
}
cursor = getBlock(cursor.getBlockPrev());
}
long elapsed = System.currentTimeMillis() - now;
if (elapsed > 50)
log.info("Difficulty transition traversal took {}msec", elapsed);
transitionTime = cursor.getBlockTime();
}
block.verifyDifficultyFromPreviousBlock(prev, transitionTime);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
block.setMain(true);
prev = block;
}
if (rollbackBlockNo > 0) {
this.rollbackBlock(rollbackBlockNo);
}
this.addBlocks(blocks);
for (Block block : blocks) {
AbstractDb.txProvider.confirmTx(block.getBlockNo(), block.getTxHashes());
}
this.lastBlock = blocks.get(blocks.size() - 1);
return blocks.size();
}
private void extendMainChain(Block block) {
if (Arrays.equals(block.getBlockPrev(), this.lastBlock.getBlockHash())) {
block.setMain(true);
this.addBlock(block);
this.lastBlock = block;
}
}
private boolean inMainChain(Block block) {
Block b = this.lastBlock;
while (b != null && b.getBlockNo() > block.getBlockNo()) {
b = AbstractDb.blockProvider.getBlock(b.getBlockPrev());
}
return b != null && Arrays.equals(b.getBlockHash(), block.getBlockHash());
}
private void addBlock(Block block) {
AbstractDb.blockProvider.addBlock(block);
}
private void addOrphan(Block block) {
block.setMain(false);
this.addBlock(block);
this.lastOrphanBlock = block;
}
private Block getSameParent(Block block1, Block block2) {
Block b1 = block1;
Block b2 = block2;
while (b1 != null && b2 != null && !Arrays.equals(b1.getBlockHash(), b2.getBlockHash())) {
if (b1.getBlockNo() == 0 || b1.getBlockNo() >= b2.getBlockNo()) {
b1 = AbstractDb.blockProvider.getBlock(b1.getBlockPrev());
}
if (b1.getBlockNo() < b2.getBlockNo()) {
b2 = AbstractDb.blockProvider.getBlock(b2.getBlockPrev());
}
}
return b1;
}
private void forkMainChain(Block forkStartBlock, Block lastBlock) {
Block b = this.lastBlock;
Block next = lastBlock;
while (!Arrays.equals(b.getBlockHash(), forkStartBlock.getBlockHash())) {
next = AbstractDb.blockProvider.getOrphanBlockByPrevHash(b.getBlockPrev());
AbstractDb.blockProvider.updateBlock(b.getBlockHash(), false);
b = AbstractDb.blockProvider.getMainChainBlock(b.getBlockPrev());
this.lastBlock = b;
}
b = next;
AbstractDb.blockProvider.updateBlock(next.getBlockHash(), true);
this.lastBlock = next;
while (!Arrays.equals(b.getBlockHash(), lastBlock.getBlockPrev())) {
AbstractDb.blockProvider.updateBlock(b.getBlockHash(), true);
this.lastBlock = b;
b = AbstractDb.blockProvider.getOrphanBlockByPrevHash(b.getBlockHash());
}
lastBlock.setMain(true);
this.addBlock(lastBlock);
this.lastBlock = lastBlock;
}
public List<Block> getLimitBlocks(int limit) {
return AbstractDb.blockProvider.getLimitBlocks(limit);
}
}