Java tutorial
/* * Copyright 2013 bits of proof zrt. * * 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 com.bitsofproof.supernode.core; import java.math.BigInteger; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.locks.ReentrantReadWriteLock; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.support.TransactionCallback; import org.springframework.transaction.support.TransactionTemplate; import com.bitsofproof.supernode.api.BloomFilter; import com.bitsofproof.supernode.api.BloomFilter.UpdateMode; import com.bitsofproof.supernode.api.ByteUtils; import com.bitsofproof.supernode.api.ByteVector; import com.bitsofproof.supernode.api.ColorRules; import com.bitsofproof.supernode.api.ColorRules.ColoredCoin; import com.bitsofproof.supernode.api.Hash; import com.bitsofproof.supernode.api.ScriptFormat; import com.bitsofproof.supernode.api.ScriptFormat.Token; import com.bitsofproof.supernode.api.ValidationException; import com.bitsofproof.supernode.api.WireFormat; import com.bitsofproof.supernode.model.Blk; import com.bitsofproof.supernode.model.Head; import com.bitsofproof.supernode.model.StoredColor; import com.bitsofproof.supernode.model.Tx; import com.bitsofproof.supernode.model.TxIn; import com.bitsofproof.supernode.model.TxOut; public abstract class CachedBlockStore implements BlockStore { private static final Logger log = LoggerFactory.getLogger(CachedBlockStore.class); private static final long MAX_BLOCK_SIGOPS = 20000; // not allowed to branch further back on trunk private static final int FORCE_TRUNK = 100; private static final long MIN_RELAY_TX_FEE = 10000; private static final long KB_RELAY_TX_FEE = 50000; private static final int COINBASE_MATURITY = 100; private static final Map<Integer, String> checkPoints = new HashMap<Integer, String>(); private static final int lastCheckPoint; static { checkPoints.put(11111, "0000000069e244f73d78e8fd29ba2fd2ed618bd6fa2ee92559f542fdb26e7c1d"); checkPoints.put(33333, "000000002dd5588a74784eaa7ab0507a18ad16a236e7b1ce69f00d7ddfb5d0a6"); checkPoints.put(74000, "0000000000573993a3c9e41ce34471c079dcf5f52a0e824a81e7f953b8661a20"); checkPoints.put(105000, "00000000000291ce28027faea320c8d2b054b2e0fe44a773f3eefb151d6bdc97"); checkPoints.put(134444, "00000000000005b12ffd4cd315cd34ffd4a594f430ac814c91184a0d42d2b0fe"); checkPoints.put(168000, "000000000000099e61ea72015e79632f216fe6cb33d7899acb35b75c8303b763"); checkPoints.put(193000, "000000000000059f452a5f7340de6682a977387c17010ff6e6c3bd83ca8b1317"); checkPoints.put(210000, "000000000000048b95347e83192f69cf0366076336c639f9b7228e9ba171342e"); checkPoints.put(222222, "00000000000000b8b49d0b61b14994b5c0a511c4b48a1e251ff2b479b2e6f678"); checkPoints.put(232000, "000000000000018f47636e1c3a946db77624880ae484ffb0233f5aac6316b3bb"); lastCheckPoint = 232000; } private Chain chain; @Autowired PlatformTransactionManager transactionManager; private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); private boolean enforceV2Block = false; protected CachedHead currentHead = null; protected final Map<String, CachedBlock> cachedBlocks = new HashMap<String, CachedBlock>(); protected final Map<Long, CachedHead> cachedHeads = new HashMap<Long, CachedHead>(); protected final Map<String, StoredColor> cachedColors = new HashMap<String, StoredColor>(); private final ImplementTxOutCache cachedUTXO = new ImplementTxOutCache(); private final List<TrunkListener> trunkListener = new ArrayList<TrunkListener>(); private final ExecutorService inputProcessor = Executors .newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2); private final ExecutorService transactionsProcessor = Executors .newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2); @Override public ValidationException runInCacheContext(CacheContextRunnable runnable) { try { lock.readLock().lock(); runnable.run(cachedUTXO); return null; } catch (ValidationException e) { return e; } finally { lock.readLock().unlock(); } } protected abstract void clearStore(); protected abstract void startBatch(); protected abstract void endBatch(); protected abstract void cancelBatch(); protected abstract void cacheChain(); protected abstract void cacheHeads(); protected abstract void cacheColors(); protected abstract void cacheUTXO(int lookback, TxOutCache cache); protected abstract List<TxOut> findTxOuts(Map<String, HashSet<Long>> need) throws ValidationException; protected abstract void checkBIP30Compliance(Set<String> txs, int untilHeight) throws ValidationException; protected abstract void backwardCache(Blk b, TxOutCache cache, boolean modify) throws ValidationException; protected abstract void forwardCache(Blk b, TxOutCache cache, boolean modify) throws ValidationException; protected abstract TxOut getSourceReference(TxOut source) throws ValidationException; protected abstract void insertBlock(Blk b) throws ValidationException; protected abstract void insertHead(Head head); protected abstract Head updateHead(Head head); protected abstract Head retrieveHead(CachedHead cached) throws ValidationException; protected abstract Blk retrieveBlock(CachedBlock cached) throws ValidationException; protected abstract Blk retrieveBlockHeader(CachedBlock cached) throws ValidationException; protected abstract void updateColor(TxOut root, String fungibleName) throws ValidationException; @Override public void addTrunkListener(TrunkListener listener) { trunkListener.add(listener); } protected static class CachedHead { private Long id; private CachedBlock last; private long chainWork; private int height; private CachedHead previous; private int previousHeight; private final Set<CachedBlock> blocks = new HashSet<CachedBlock>(); public CachedHead() { } public Long getId() { return id; } public void setId(Long id) { this.id = id; } public long getChainWork() { return chainWork; } public long getHeight() { return height; } public void setChainWork(long chainWork) { this.chainWork = chainWork; } public void setHeight(int height) { this.height = height; } public Set<CachedBlock> getBlocks() { return blocks; } public CachedHead getPrevious() { return previous; } public void setPrevious(CachedHead previous) { this.previous = previous; } public CachedBlock getLast() { return last; } public void setLast(CachedBlock last) { this.last = last; } public int getPreviousHeight() { return previousHeight; } public void setPreviousHeight(int previousHeight) { this.previousHeight = previousHeight; } } protected static class CachedBlock { public CachedBlock(String hash, Long id, CachedBlock previous, long time, int height, int version, byte[] filterMap, int filterFunctions) { this.hash = hash; this.id = id; this.previous = previous; this.time = time; this.height = height; this.version = version; this.filterMap = filterMap; this.filterFunctions = filterFunctions; } private final String hash; private final Long id; private final CachedBlock previous; private final long time; private final int height; private final int version; private final byte[] filterMap; private final int filterFunctions; public byte[] getFilterMap() { return filterMap; } public int getFilterFunctions() { return filterFunctions; } public Long getId() { return id; } public CachedBlock getPrevious() { return previous; } public long getTime() { return time; } public String getHash() { return hash; } public int getHeight() { return height; } public int getVersion() { return version; } @Override public int hashCode() { return hash.hashCode(); } @Override public boolean equals(Object o) { if (o == null) { return false; } if (o == this) { return true; } if (o instanceof CachedBlock) { return hash.equals(((CachedBlock) o).hash); } return false; } } @Override @Transactional(propagation = Propagation.REQUIRED, readOnly = true) public void cache(Chain chain, int size) throws ValidationException { this.chain = chain; try { lock.writeLock().lock(); log.trace("Cache heads..."); cacheHeads(); log.trace("Cache colors..."); cacheColors(); log.trace("Cache chain..."); cacheChain(); if (size > 0) { log.trace("Cache UTXO set ..."); cacheUTXO(size, cachedUTXO); } log.trace("Cache filled."); } finally { lock.writeLock().unlock(); } } private static final int MAX_MATCH_SET = 1000; @Transactional(propagation = Propagation.REQUIRED, rollbackFor = { Exception.class }, readOnly = true) @Override public void filterTransactions(List<byte[]> data, UpdateMode update, long after, TransactionProcessor processor) throws ValidationException { List<CachedBlock> blocks = new ArrayList<CachedBlock>(); try { lock.readLock().lock(); CachedBlock q = currentHead.getLast(); CachedBlock p = q.previous; while (p != null) { if (after == 0 || q.time > after) { blocks.add(q); } q = p; p = q.previous; } Collections.reverse(blocks); } finally { lock.readLock().unlock(); } Set<ByteVector> matchSet = new HashSet<ByteVector>(); for (byte[] d : data) { matchSet.add(new ByteVector(d)); } for (CachedBlock cb : blocks) { if (matchSet.size() > MAX_MATCH_SET) { throw new ValidationException("Match set too big for filterTransactions. Use scan instead"); } boolean found = false; BloomFilter filter = new BloomFilter(cb.filterMap, cb.filterFunctions, 0, UpdateMode.none); for (ByteVector v : matchSet) { if (filter.contains(v.toByteArray())) { found = true; break; } } if (!found) { continue; } try { Blk b = retrieveBlock(cb); for (Tx t : b.getTransactions()) { found = false; for (TxOut o : t.getOutputs()) { try { for (Token token : ScriptFormat.parse(o.getScript())) { if (token.data != null && matchSet.contains(new ByteVector(token.data))) { if (update == UpdateMode.all) { matchSet.add(new ByteVector( BloomFilter.serializedOutpoint(t.getHash(), o.getIx()))); } else if (update == UpdateMode.keys) { if (ScriptFormat.isPayToKey(o.getScript()) || ScriptFormat.isMultiSig(o.getScript())) { matchSet.add(new ByteVector( BloomFilter.serializedOutpoint(t.getHash(), o.getIx()))); } } found = true; break; } } } catch (Exception e) { // best effort } } if (!found) { for (TxIn i : t.getInputs()) { if (!i.getSourceHash().equals(Hash.ZERO_HASH_STRING)) { ByteVector outpoint = new ByteVector( BloomFilter.serializedOutpoint(i.getSourceHash(), i.getIx())); if (matchSet.contains(outpoint)) { found = true; // it would be a double spend if it were there again. matchSet.remove(outpoint); break; } try { for (Token token : ScriptFormat.parse(i.getScript())) { if (token.data != null && matchSet.contains(new ByteVector(token.data))) { found = true; break; } } } catch (Exception e) { // best effort } } } } if (found) { processor.process(t); } } } catch (ValidationException e) { log.error("Error while scanning blocks", e); } } processor.process(null); } private boolean isBlockOnBranch(CachedBlock block, CachedHead branch, int untilHeight) { if (branch.getBlocks().contains(block)) { return block.getHeight() <= untilHeight; } if (branch.getPrevious() == null) { return false; } return isBlockOnBranch(block, branch.getPrevious(), branch.getPreviousHeight()); } protected boolean isOnTrunk(String block) { try { lock.readLock().lock(); CachedBlock b = cachedBlocks.get(block); return isBlockOnBranch(b, currentHead, (int) currentHead.getHeight()); } finally { lock.readLock().unlock(); } } private boolean isSuperMajority(int minVersion, CachedBlock from, int nRequired, int nToCheck) { int nFound = 0; for (int i = 0; i < nToCheck && nFound < nRequired && from != null; i++) { if (from.version >= minVersion) { ++nFound; } from = from.previous; } return (nFound >= nRequired); } @Override public boolean isStoredBlock(String hash) { try { lock.readLock().lock(); return cachedBlocks.get(hash) != null; } finally { lock.readLock().unlock(); } } @Override public long getChainHeight() { try { lock.readLock().lock(); return currentHead.getHeight(); } finally { lock.readLock().unlock(); } } @Override public List<String> getInventory(List<String> locator, String last, int limit) { try { lock.readLock().lock(); List<String> inventory = new LinkedList<String>(); CachedBlock curr = currentHead.getLast(); CachedBlock prev = curr.getPrevious(); if (!last.equals(Hash.ZERO_HASH.toString())) { while (prev != null && !curr.getHash().equals(last)) { curr = prev; prev = curr.getPrevious(); } } do { if (locator.contains(curr.getHash()) || curr.getHeight() < 1) { break; } inventory.add(0, curr.getHash()); if (inventory.size() > limit) { inventory.remove(limit); } curr = prev; prev = curr.getPrevious(); } while (prev != null); return inventory; } finally { lock.readLock().unlock(); } } @Override public List<String> getLocator() { try { lock.readLock().lock(); List<String> locator = new ArrayList<String>(); CachedBlock curr = currentHead.getLast(); locator.add(curr.getHash()); CachedBlock prev = curr.getPrevious(); for (int i = 0, step = 1; prev != null; ++i) { for (int j = 0; prev != null && j < step; ++j) { curr = prev; prev = curr.getPrevious(); } locator.add(curr.getHash()); if (i >= 10) { step *= 2; } } return locator; } finally { lock.readLock().unlock(); } } @Override public long getPeriodLength(String previousHash, int reviewPeriod) { try { lock.readLock().lock(); CachedBlock cachedPrevious = cachedBlocks.get(previousHash); return computePeriodLength(cachedPrevious, cachedPrevious.getTime(), reviewPeriod); } finally { lock.readLock().unlock(); } } private static class TransactionContext { Blk block; BigInteger blkSumInput = BigInteger.ZERO; BigInteger blkSumOutput = BigInteger.ZERO; int nsigs = 0; boolean coinbase = true; TxOutCache resolvedInputs = new ImplementTxOutCache(); } @Override public void storeBlock(final Blk b) throws ValidationException { try { // have to lock before transaction starts and unlock after finished. // otherwise updates to transient vs. persistent structures are out of sync for a // concurrent tx. This does not apply to methods using MANDATORY transaction annotation // context since they must have been invoked within a transaction. lock.writeLock().lock(); ValidationException e = new TransactionTemplate(transactionManager) .execute(new TransactionCallback<ValidationException>() { @Override public ValidationException doInTransaction(TransactionStatus status) { try { startBatch(); lockedStoreBlock(b); endBatch(); } catch (ValidationException e) { cancelBatch(); status.setRollbackOnly(); return e; } catch (Exception e) { cancelBatch(); status.setRollbackOnly(); return new ValidationException(e); } return null; } }); if (e != null) { throw e; } } finally { lock.writeLock().unlock(); } } private void lockedStoreBlock(Blk b) throws ValidationException { CachedBlock cached = cachedBlocks.get(b.getHash()); if (cached != null) { return; } log.trace("Start storing block " + b.getHash()); // find previous block CachedBlock cachedPrevious = cachedBlocks.get(b.getPreviousHash()); if (cachedPrevious == null) { throw new ValidationException("Does not connect to a known block " + b.getHash()); } Blk prev = null; prev = retrieveBlockHeader(cachedPrevious); if (b.getCreateTime() > (System.currentTimeMillis() / 1000) * 2 * 60 * 60 || b.getCreateTime() <= getMedianTimePast(cachedPrevious)) { throw new ValidationException("Block timestamp out of bounds " + b.getHash()); } boolean checkV2BlockCoinBase = false; if (chain.isProduction()) { if (enforceV2Block || isSuperMajority(2, cachedPrevious, 950, 1000)) { if (!enforceV2Block) { log.trace("Majority for V2 blocks reached, enforcing"); } enforceV2Block = true; if (b.getVersion() < 2) { throw new ValidationException("Rejecting version 1 block " + b.getHash()); } } checkV2BlockCoinBase = b.getVersion() >= 2 && isSuperMajority(2, cachedPrevious, 750, 1000); } Head head; CachedHead cachedPreviousHead = cachedHeads.get(prev.getHeadId()); Head previousHead = retrieveHead(cachedPreviousHead); if (previousHead.getLeaf().equals(prev.getHash())) { // continuing head = previousHead; head.setLeaf(b.getHash()); head.setHeight(head.getHeight() + 1); head.setChainWork(prev.getChainWork() + Difficulty.getDifficulty(b.getDifficultyTarget(), chain)); head = updateHead(head); } else { // branching head = new Head(); head.setPreviousId(prev.getHeadId()); head.setPreviousHeight(prev.getHeight()); head.setLeaf(b.getHash()); head.setHeight(prev.getHeight() + 1); head.setChainWork(prev.getChainWork() + Difficulty.getDifficulty(b.getDifficultyTarget(), chain)); insertHead(head); } b.setHeadId(head.getId()); b.setHeight(head.getHeight()); b.setChainWork(head.getChainWork()); if (b.getHeight() >= chain.getDifficultyReviewBlocks() && b.getHeight() % chain.getDifficultyReviewBlocks() == 0) { long periodLength = computePeriodLength(cachedPrevious, prev.getCreateTime(), chain.getDifficultyReviewBlocks()); long next = Difficulty.getNextTarget(periodLength, prev.getDifficultyTarget(), chain); if (chain.isProduction() && next != b.getDifficultyTarget()) { throw new ValidationException( "Difficulty does not match expectation " + b.getHash() + " " + b.toWireDump()); } } else { if (chain.isProduction() && b.getDifficultyTarget() != prev.getDifficultyTarget()) { throw new ValidationException("Illegal attempt to change difficulty " + b.getHash()); } } b.checkHash(); if (chain.isProduction() && checkPoints.containsKey(b.getHeight())) { if (!checkPoints.get(b.getHeight()).equals(b.getHash())) { throw new ValidationException("Checkpoint missed"); } } BigInteger hashAsInteger = new Hash(b.getHash()).toBigInteger(); if (chain.isProduction() && hashAsInteger.compareTo(Difficulty.getTarget(b.getDifficultyTarget())) > 0) { throw new ValidationException( "Insufficuent proof of work for current difficulty " + b.getHash() + " " + b.toWireDump()); } b.parseTransactions(); if (b.getTransactions().isEmpty()) { throw new ValidationException("Block must have transactions " + b.getHash() + " " + b.toWireDump()); } for (Tx t : b.getTransactions()) { if (!isFinal(t, b)) { throw new ValidationException("Transactions in a block must be final"); } } b.checkMerkleRoot(); ImplementTxOutCacheDelta deltaUTXO = new ImplementTxOutCacheDelta(cachedUTXO); CachedBlock trunkBlock = cachedPrevious; List<CachedBlock> pathFromTrunkToPrev = new ArrayList<CachedBlock>(); while (!isOnTrunk(trunkBlock.getHash())) { pathFromTrunkToPrev.add(trunkBlock); trunkBlock = trunkBlock.getPrevious(); } Collections.reverse(pathFromTrunkToPrev); if (trunkBlock.getHeight() < (currentHead.getLast().getHeight() - FORCE_TRUNK)) { throw new ValidationException("Attempt to build on or create a branch too far back in history"); } if (currentHead.getLast() != cachedPrevious) { CachedBlock q = currentHead.getLast(); CachedBlock p = q.previous; while (!q.getHash().equals(trunkBlock.getHash())) { Blk block = retrieveBlock(q); backwardCache(block, deltaUTXO, false); q = p; p = q.previous; } for (CachedBlock block : pathFromTrunkToPrev) { forwardCache(retrieveBlock(block), deltaUTXO, false); } } final TransactionContext tcontext = new TransactionContext(); tcontext.block = b; tcontext.resolvedInputs = deltaUTXO; log.trace("resolving inputs for block " + b.getHash()); Set<String> txs = new HashSet<String>(); int numberOfOutputs = 0; for (Tx t : b.getTransactions()) { txs.add(t.getHash()); resolveInputs(tcontext.resolvedInputs, b.getHeight(), t); for (TxOut o : t.getOutputs()) { o.setHeight(b.getHeight()); tcontext.resolvedInputs.add(o); ++numberOfOutputs; } } if (!chain.isProduction() || b.getHeight() > lastCheckPoint || chain.checkBeforeCheckpoint()) { log.trace("validating block " + b.getHash()); // BIP30 if (currentHead.getLast() != cachedPrevious) { for (CachedBlock bl : pathFromTrunkToPrev) { Blk block = retrieveBlock(bl); for (Tx t : block.getTransactions()) { if (txs.contains(t.getHash())) { throw new ValidationException( "BIP30 violation: block contains spent tx " + t.getHash()); } } } } checkBIP30Compliance(txs, trunkBlock.height); List<Callable<TransactionValidationException>> callables = new ArrayList<Callable<TransactionValidationException>>(); for (final Tx t : b.getTransactions()) { if (tcontext.coinbase) { if (!isCoinBase(t)) { throw new ValidationException("The first transaction of a block must be coinbase"); } try { if (checkV2BlockCoinBase) { ScriptFormat.Reader reader = new ScriptFormat.Reader(t.getInputs().get(0).getScript()); int len = reader.readByte(); if (ScriptFormat.intValue(reader.readBytes(len)) != b.getHeight()) { throw new ValidationException("Block height mismatch in coinbase"); } } validateTransaction(tcontext, t); } catch (TransactionValidationException e) { throw new ValidationException(e.getMessage() + " " + t.toWireDump(), e); } tcontext.coinbase = false; } else { if (isCoinBase(t)) { throw new ValidationException("Only the first transaction of a block can be coinbase"); } callables.add(new Callable<TransactionValidationException>() { @Override public TransactionValidationException call() { try { validateTransaction(tcontext, t); } catch (TransactionValidationException e) { return e; } catch (Exception e) { return new TransactionValidationException(e, t); } return null; } }); } } try { for (Future<TransactionValidationException> e : transactionsProcessor.invokeAll(callables)) { try { if (e.get() != null) { throw new ValidationException(e.get().getMessage() + " " + e.get().getIn() + " " + e.get().getTx().toWireDump(), e.get()); } } catch (ExecutionException e1) { throw new ValidationException("corrupted transaction processor", e1); } } } catch (InterruptedException e1) { throw new ValidationException("interrupted", e1); } if (tcontext.nsigs > MAX_BLOCK_SIGOPS) { throw new ValidationException("too many signatures in this block "); } // block reward could actually be less... as in 0000000000004c78956f8643262f3622acf22486b120421f893c0553702ba7b5 if (tcontext.blkSumOutput.subtract(tcontext.blkSumInput).longValue() > chain .getRewardForHeight(b.getHeight())) { throw new ValidationException("Invalid block reward " + b.getHash() + " " + b.toWireDump()); } } // this is last loop before persist since modifying the entities. BloomFilter filter = BloomFilter.createOptimalFilter(Math.max(5 * numberOfOutputs, 500), 1.0e-8, 0, UpdateMode.none); for (Tx t : b.getTransactions()) { t.setBlock(b); List<ColoredCoin> inputCoins = new ArrayList<ColoredCoin>(); for (TxIn i : t.getInputs()) { if (!i.getSourceHash().equals(Hash.ZERO_HASH_STRING)) { filter.addOutpoint(i.getSourceHash(), i.getIx()); try { for (Token token : ScriptFormat.parse(i.getScript())) { if (token.data != null) { filter.add(token.data); } } } catch (Exception e) { // this is best effort } TxOut source = tcontext.resolvedInputs.get(i.getSourceHash(), i.getIx()); if (source.getId() == null) { i.setSource(source); } else { i.setSource(getSourceReference(source)); } ColoredCoin c = new ColoredCoin(); c.color = source.getColor(); c.value = source.getValue(); inputCoins.add(c); } } List<ColoredCoin> outputCoins = new ArrayList<ColoredCoin>(); for (TxOut o : t.getOutputs()) { ColoredCoin c = new ColoredCoin(); c.color = null; c.value = o.getValue(); outputCoins.add(c); } ColorRules.colorOutput(inputCoins, outputCoins); Iterator<ColoredCoin> i = outputCoins.iterator(); for (TxOut o : t.getOutputs()) { o.setColor(i.next().color); try { for (Token token : ScriptFormat.parse(o.getScript())) { if (token.data != null) { filter.add(token.data); } } } catch (Exception e) { // this is best effort } o.setTxHash(t.getHash()); o.setHeight(b.getHeight()); } } b.setFilterFunctions((int) filter.getHashFunctions()); b.setFilterMap(filter.getFilter()); log.trace("storing block " + b.getHash()); insertBlock(b); // modify transient caches only after persistent changes CachedBlock m = new CachedBlock(b.getHash(), b.getId(), cachedBlocks.get(b.getPreviousHash()), b.getCreateTime(), b.getHeight(), (int) b.getVersion(), b.getFilterMap(), b.getFilterFunctions()); cachedBlocks.put(b.getHash(), m); CachedHead usingHead = cachedHeads.get(head.getId()); if (usingHead == null) { cachedHeads.put(head.getId(), usingHead = new CachedHead()); usingHead.id = head.getId(); usingHead.previous = cachedHeads.get(head.getPreviousId()); usingHead.previousHeight = b.getHeight() - 1; } usingHead.setChainWork(b.getChainWork()); usingHead.setHeight(b.getHeight()); usingHead.getBlocks().add(m); final List<Blk> removedBlocks = new ArrayList<Blk>(); final List<Blk> addedBlocks = new ArrayList<Blk>(); if (ByteUtils.isLessThanUnsigned(currentHead.getChainWork(), usingHead.getChainWork())) { // we have a new trunk CachedBlock p = currentHead.getLast(); CachedBlock q = p.previous; while (!p.equals(trunkBlock)) { Blk block = retrieveBlock(p); backwardCache(block, cachedUTXO, true); removedBlocks.add(block); p = q; q = p.previous; } List<CachedBlock> pathToNewHead = new ArrayList<CachedBlock>(); p = m; q = p.previous; while (!q.equals(trunkBlock)) { pathToNewHead.add(q); p = q; q = p.previous; } Collections.reverse(pathToNewHead); for (CachedBlock cb : pathToNewHead) { Blk block = retrieveBlock(cb); forwardCache(block, cachedUTXO, true); addedBlocks.add(block); } forwardCache(b, cachedUTXO, true); addedBlocks.add(b); currentHead = usingHead; } else if (currentHead.getLast() == cachedPrevious) { forwardCache(b, cachedUTXO, true); addedBlocks.add(b); currentHead = usingHead; } usingHead.setLast(m); log.debug("stored block " + b.getHeight() + " " + b.getHash()); if (!removedBlocks.isEmpty() || !addedBlocks.isEmpty()) { for (TrunkListener l : trunkListener) { l.trunkUpdate(removedBlocks, addedBlocks); } } } private boolean isFinal(Tx t, Blk b) { if (t.getLockTime() == 0) { return true; } long nBlockTime = b.getCreateTime(); if (nBlockTime == 0) { nBlockTime = System.currentTimeMillis() / 1000; } if (ByteUtils.isLessThanUnsigned(t.getLockTime(), (ByteUtils.isLessThanUnsigned(t.getLockTime(), 500000000) ? (int) b.getHeight() : nBlockTime))) { return true; } for (TxIn in : t.getInputs()) { if (in.getSequence() != 0xFFFFFFFFL) { return false; } } return true; } private long getMedianTimePast(CachedBlock prev) { List<Long> times = new ArrayList<Long>(); CachedBlock c = prev; CachedBlock p = c.previous; int i = 0; while (p != null && i++ < 11) { times.add(c.time); c = p; p = c.previous; } if (times.size() == 0) { return 0; } Collections.sort(times); return times.get(Math.min(5, times.size() - 1)); } private long computePeriodLength(CachedBlock cachedPrevious, long previousTime, int reviewPeriod) { long periodLength = previousTime; CachedBlock c = null; CachedBlock p = cachedPrevious; for (int i = 0; i < reviewPeriod - 1; ++i) { c = p; p = c.getPrevious(); } periodLength -= p.getTime(); return periodLength; } private void resolveInputs(TxOutCache resolvedInputs, int blockHeight, Tx t) throws ValidationException { resolveInputsUsingUTXOCache(resolvedInputs, t); resolveInputsUsingDB(resolvedInputs, t); checkInputResolution(resolvedInputs, blockHeight, t); } private void resolveInputsUsingUTXOCache(TxOutCache resolvedInputs, Tx t) throws ValidationException { for (final TxIn i : t.getInputs()) { if (!i.getSourceHash().equals(Hash.ZERO_HASH_STRING)) { resolvedInputs.copy(cachedUTXO, i.getSourceHash()); } } } private void resolveInputsUsingDB(TxOutCache resolvedInputs, Tx t) throws ValidationException { Map<String, HashSet<Long>> need = new HashMap<String, HashSet<Long>>(); for (final TxIn i : t.getInputs()) { if (!i.getSourceHash().equals(Hash.ZERO_HASH_STRING)) { if (resolvedInputs.get(i.getSourceHash(), i.getIx()) == null) { HashSet<Long> ixs = need.get(i.getSourceHash()); if (ixs == null) { ixs = new HashSet<Long>(); need.put(i.getSourceHash(), ixs); } ixs.add(i.getIx()); } } } if (!need.isEmpty()) { for (TxOut o : findTxOuts(need)) { resolvedInputs.add(o); } } } private void checkInputResolution(TxOutCache resolvedInputs, int height, Tx t) throws ValidationException { for (final TxIn i : t.getInputs()) { if (!i.getSourceHash().equals(Hash.ZERO_HASH_STRING)) { TxOut out = resolvedInputs.use(i.getSourceHash(), i.getIx()); if (out == null) { throw new ValidationException("Transaction refers to unknown or spent output " + i.getSourceHash() + " [" + i.getIx() + "] " + t.toWireDump()); } if (height != 0 && out.isCoinbase()) { if (!chain.allowImmediateCoinbaseSpend() && out.getHeight() > height - COINBASE_MATURITY) { throw new ValidationException("coinbase spent too early " + t.toWireDump()); } } } } } private boolean checkForRelay(Tx t, TxOutCache resolvedInputs) throws ValidationException { if (t.getVersion() != 1) { throw new ValidationException("Transaction version must be 1"); } if (t.getInputs() == null || t.getInputs().isEmpty()) { throw new TransactionValidationException("a transaction must have inputs", t); } if (t.getOutputs() == null || t.getOutputs().isEmpty()) { throw new TransactionValidationException("a transaction must have outputs", t); } long fee = 0; for (TxIn in : t.getInputs()) { if (in.getScript().length > 500) { throw new ValidationException("script length limit exceeded"); } if (!ScriptFormat.isPushOnly(in.getScript())) { throw new ValidationException("input script should be push only"); } fee += resolvedInputs.get(in.getSourceHash(), in.getIx()).getValue(); } for (TxOut out : t.getOutputs()) { if (!ScriptFormat.isStandard(out.getScript())) { throw new ValidationException("not a standard output script"); } if (out.getValue() == 0) { throw new ValidationException("zero output"); } fee -= out.getValue(); } // This node will not relay transactions not paying a minimal fee. WireFormat.Writer writer = new WireFormat.Writer(); t.toWire(writer); return fee >= Math.max(MIN_RELAY_TX_FEE, writer.toByteArray().length / 1024 * KB_RELAY_TX_FEE); } private boolean isCoinBase(Tx t) { return t.getInputs().size() == 1 && t.getInputs().get(0).getSourceHash().equals(Hash.ZERO_HASH_STRING); } private void validateTransaction(final TransactionContext tcontext, final Tx t) throws TransactionValidationException { if (t.getInputs() == null || t.getInputs().isEmpty()) { throw new TransactionValidationException("a transaction must have inputs", t); } if (t.getOutputs() == null || t.getOutputs().isEmpty()) { throw new TransactionValidationException("a transaction must have outputs", t); } if (isCoinBase(t)) { if (tcontext.block == null) { throw new TransactionValidationException("coinbase only allowed in a block", t); } if (t.getInputs().get(0).getScript().length < 2 || t.getInputs().get(0).getScript().length > 100) { throw new TransactionValidationException("coinbase script length out of bounds", t); } } long sumOut = 0; for (TxOut o : t.getOutputs()) { if (o.getValue() < 0) { throw new TransactionValidationException("negative output is not allowed", t); } if (o.getValue() > Tx.MAX_MONEY) { throw new TransactionValidationException("output too high", t); } synchronized (tcontext) { tcontext.nsigs += ScriptFormat.sigOpCount(o.getScript(), false); tcontext.blkSumOutput = tcontext.blkSumOutput.add(BigInteger.valueOf(o.getValue())); } sumOut += o.getValue(); } if (isCoinBase(t) == false) { long sumIn = 0; int inNumber = 0; List<Callable<TransactionValidationException>> callables = new ArrayList<Callable<TransactionValidationException>>(); Map<String, HashSet<Long>> inputUse = new HashMap<String, HashSet<Long>>(); for (TxIn i : t.getInputs()) { HashSet<Long> seen = inputUse.get(i.getSourceHash()); if (seen == null) { inputUse.put(i.getSourceHash(), seen = new HashSet<Long>()); } if (seen.contains(i.getIx())) { throw new TransactionValidationException("duplicate input", t); } seen.add(i.getIx()); TxOut source = tcontext.resolvedInputs.get(i.getSourceHash(), i.getIx()); sumIn += source.getValue(); try { synchronized (tcontext) { if (ScriptFormat.isPayToScriptHash(source.getScript())) { ScriptFormat.Tokenizer tokenizer = new ScriptFormat.Tokenizer(i.getScript()); byte[] last = null; while (tokenizer.hashMoreElements()) { last = tokenizer.nextToken().data; } tcontext.nsigs += ScriptFormat.sigOpCount(last, true); } else { tcontext.nsigs += ScriptFormat.sigOpCount(i.getScript(), false); } tcontext.blkSumInput = tcontext.blkSumInput.add(BigInteger.valueOf(source.getValue())); } } catch (ValidationException e) { throw new TransactionValidationException(e, t); } final ScriptEvaluation evaluation = new ScriptEvaluation(t, inNumber++, source); callables.add(new Callable<TransactionValidationException>() { @Override public TransactionValidationException call() throws Exception { try { if (!evaluation.evaluate(chain.isProduction())) { return new TransactionValidationException( "The transaction script does not evaluate to true in input", t, evaluation.getInr()); } } catch (Exception e) { return new TransactionValidationException(e, t, evaluation.getInr()); } return null; } }); } if (sumOut > sumIn) { throw new TransactionValidationException("Transaction value out more than in", t); } List<Future<TransactionValidationException>> results; try { results = inputProcessor.invokeAll(callables); } catch (InterruptedException e1) { throw new TransactionValidationException(e1, t); } for (Future<TransactionValidationException> r : results) { TransactionValidationException ex; try { ex = r.get(); } catch (InterruptedException e) { throw new TransactionValidationException(e, t); } catch (ExecutionException e) { throw new TransactionValidationException(e, t); } if (ex != null) { throw ex; } } } } @Override public String getHeadHash() { try { lock.readLock().lock(); return currentHead.getLast().getHash(); } finally { lock.readLock().unlock(); } } @Transactional(propagation = Propagation.REQUIRED, rollbackFor = { Exception.class }) @Override public void resetStore(Chain chain) throws ValidationException { log.info("Reset block store"); clearStore(); this.chain = chain; Blk genesis = chain.getGenesis(); Head h = new Head(); h.setLeaf(genesis.getHash()); h.setHeight(0); h.setChainWork(Difficulty.getDifficulty(genesis.getDifficultyTarget(), chain)); insertHead(h); genesis.setHeadId(h.getId()); insertBlock(genesis); } @Transactional(propagation = Propagation.MANDATORY, readOnly = true) @Override public Blk getBlock(String hash) throws ValidationException { CachedBlock cached = null; try { lock.readLock().lock(); cached = cachedBlocks.get(hash); if (cached == null) { return null; } } finally { lock.readLock().unlock(); } return retrieveBlock(cached); } @Transactional(propagation = Propagation.MANDATORY, readOnly = true) @Override public void resolveTransactionInputs(Tx t, TxOutCache resolvedInputs) throws ValidationException { try { lock.readLock().lock(); resolveInputs(resolvedInputs, 0, t); } finally { lock.readLock().unlock(); } } @Transactional(propagation = Propagation.MANDATORY, readOnly = true) @Override public boolean validateTransaction(Tx t, TxOutCache resolvedInputs) throws ValidationException { try { lock.readLock().lock(); if (t.getInputs() == null) { throw new ValidationException("a transaction must have inputs"); } resolveInputs(resolvedInputs, 0, t); TransactionContext tcontext = new TransactionContext(); tcontext.block = null; tcontext.coinbase = false; tcontext.nsigs = 0; tcontext.resolvedInputs = resolvedInputs; boolean relay = !chain.isProduction() || checkForRelay(t, resolvedInputs); validateTransaction(tcontext, t); return relay; } finally { lock.readLock().unlock(); } } @Override public void issueColor(final StoredColor color) throws ValidationException { try { // have to lock before transaction starts and unlock after finished. // otherwise updates to transient vs. persistent structures are out of sync for a // concurrent tx. This does not apply to methods using MANDATORY transaction annotation // context since they must have been invoked within a transaction. lock.writeLock().lock(); ValidationException e = new TransactionTemplate(transactionManager) .execute(new TransactionCallback<ValidationException>() { @Override public ValidationException doInTransaction(TransactionStatus status) { try { startBatch(); Tx tx = getTransaction(color.getTxHash()); if (tx == null) { throw new ValidationException("Unknown transaction for new color"); } TxOut out = tx.getOutputs().get(0); if (!out.isAvailable()) { throw new ValidationException("The color genesis was already spent."); } if (!ScriptFormat.isPayToAddress(out.getScript())) { throw new ValidationException("Color output should pay to address"); } List<Token> tokens = ScriptFormat.parse(out.getScript()); if (!Arrays.equals(tokens.get(2).data, Hash.keyHash(color.getPubkey()))) { throw new ValidationException("Color key does not match output address"); } if (!color.verify()) { storeColor(color); } else { throw new ValidationException("Color is not valid"); } updateColor(out, color.getFungibleName()); endBatch(); } catch (ValidationException e) { cancelBatch(); status.setRollbackOnly(); return e; } catch (Exception e) { cancelBatch(); status.setRollbackOnly(); return new ValidationException(e); } return null; } }); if (e != null) { throw e; } } finally { lock.writeLock().unlock(); } } }