diff --git a/src/main/java/org/semux/Kernel.java b/src/main/java/org/semux/Kernel.java index 5789dbbc8..93541dd29 100644 --- a/src/main/java/org/semux/Kernel.java +++ b/src/main/java/org/semux/Kernel.java @@ -76,7 +76,6 @@ public enum State { protected State state = State.STOPPED; - protected final ReentrantReadWriteLock stateLock = new ReentrantReadWriteLock(); protected Config config; protected Genesis genesis; @@ -362,7 +361,7 @@ public synchronized void stop() { client.close(); // make sure no thread is reading/writing the state - ReentrantReadWriteLock.WriteLock lock = stateLock.writeLock(); + ReentrantReadWriteLock.WriteLock lock = chain.getStateLock().writeLock(); lock.lock(); try { for (DatabaseName name : DatabaseName.values()) { @@ -456,15 +455,6 @@ public Config getConfig() { return config; } - /** - * Returns the state lock. - * - * @return - */ - public ReentrantReadWriteLock getStateLock() { - return stateLock; - } - /** * Returns the syncing manager. * diff --git a/src/main/java/org/semux/api/v2/TypeFactory.java b/src/main/java/org/semux/api/v2/TypeFactory.java index 75ec688fb..8a0f2e011 100644 --- a/src/main/java/org/semux/api/v2/TypeFactory.java +++ b/src/main/java/org/semux/api/v2/TypeFactory.java @@ -157,9 +157,7 @@ public static TransactionType transactionType(Transaction tx) { public static TransactionResultType transactionResultType(Transaction tx, TransactionResult result, long number) { // gas price is in nano sem, not wei - boolean isVMTransaction = (tx.getType() == org.semux.core.TransactionType.CREATE - || tx.getType() == org.semux.core.TransactionType.CALL); - Amount fee = isVMTransaction ? Amount.mul(result.getGasPrice(), result.getGasUsed()) : tx.getFee(); + Amount fee = tx.isVMTransaction() ? Amount.mul(result.getGasPrice(), result.getGasUsed()) : tx.getFee(); return new TransactionResultType() .blockNumber(Long.toString(number)) diff --git a/src/main/java/org/semux/config/AbstractConfig.java b/src/main/java/org/semux/config/AbstractConfig.java index f1052a160..1e5b71c68 100644 --- a/src/main/java/org/semux/config/AbstractConfig.java +++ b/src/main/java/org/semux/config/AbstractConfig.java @@ -52,7 +52,6 @@ public abstract class AbstractConfig implements Config, ChainSpec { // Chain spec // ========================= protected long maxBlockGasLimit = 10_000_000L; // 10m gas - protected int maxBlockTransactionsSize = 1024 * 1024; protected Amount minTransactionFee = MILLI_SEM.of(5); protected Amount minDelegateBurnAmount = SEM.of(1000); protected long nonVMTransactionGasCost = 21_000L; @@ -160,11 +159,6 @@ public long maxBlockGasLimit() { return maxBlockGasLimit; } - @Override - public int maxBlockTransactionsSize() { - return maxBlockTransactionsSize; - } - @Override public int maxTransactionDataSize(TransactionType type) { switch (type) { diff --git a/src/main/java/org/semux/config/ChainSpec.java b/src/main/java/org/semux/config/ChainSpec.java index c2d6c758f..ae29a0263 100644 --- a/src/main/java/org/semux/config/ChainSpec.java +++ b/src/main/java/org/semux/config/ChainSpec.java @@ -25,14 +25,6 @@ public interface ChainSpec { */ long maxBlockGasLimit(); - /** - * Returns the max total size of all transactions in a block, encoding overhead - * not counted. - * - * @return - */ - int maxBlockTransactionsSize(); - /** * Returns the max data size for the given transaction type. * diff --git a/src/main/java/org/semux/config/TestnetConfig.java b/src/main/java/org/semux/config/TestnetConfig.java index f0380cf1c..e13aa8beb 100644 --- a/src/main/java/org/semux/config/TestnetConfig.java +++ b/src/main/java/org/semux/config/TestnetConfig.java @@ -17,9 +17,6 @@ public class TestnetConfig extends AbstractConfig { public TestnetConfig(String dataDir) { super(dataDir, Network.TESTNET, Constants.TESTNET_VERSION); - // testnet allows a much larger block size for performance tuning (8MB) - this.maxBlockTransactionsSize = 8 * 1024 * 1024; - this.forkUniformDistributionEnabled = true; this.forkVirtualMachineEnabled = true; } diff --git a/src/main/java/org/semux/consensus/SemuxBft.java b/src/main/java/org/semux/consensus/SemuxBft.java index af2286497..a337a0db7 100644 --- a/src/main/java/org/semux/consensus/SemuxBft.java +++ b/src/main/java/org/semux/consensus/SemuxBft.java @@ -15,7 +15,6 @@ import java.util.Set; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock; import java.util.stream.Collectors; import org.ethereum.vm.client.BlockStore; @@ -25,7 +24,6 @@ import org.semux.config.Constants; import org.semux.consensus.SemuxBft.Event.Type; import org.semux.consensus.exception.SemuxBftException; -import org.semux.core.Amount; import org.semux.core.BftManager; import org.semux.core.Block; import org.semux.core.BlockHeader; @@ -35,7 +33,6 @@ import org.semux.core.Transaction; import org.semux.core.TransactionExecutor; import org.semux.core.TransactionResult; -import org.semux.core.TransactionType; import org.semux.core.state.AccountState; import org.semux.core.state.DelegateState; import org.semux.crypto.Hash; @@ -433,7 +430,7 @@ protected void enterFinalize() { // [2] add the block to chain logger.info(block.toString()); - applyBlock(block); + chain.importBlock(block, false); } else { sync(height + 1); } @@ -756,157 +753,132 @@ protected void clearTimerAndEvents() { protected Block proposeBlock() { long t1 = TimeUtil.currentTimeMillis(); - // construct block + // construct block template BlockHeader parent = chain.getBlockHeader(height - 1); long number = height; byte[] prevHash = parent.getHash(); long timestamp = TimeUtil.currentTimeMillis(); - /* - * in case the previous block timestamp is drifted too munch, adjust this block - * timestamp to avoid invalid blocks (triggered by timestamp rule). - * - * See https://github.com/semuxproject/semux-core/issues/1 - */ timestamp = timestamp > parent.getTimestamp() ? timestamp : parent.getTimestamp() + 1; - - byte[] data = chain.constructBlockData(); + byte[] data = chain.constructBlockHeaderDataField(); + BlockHeader tempHeader = new BlockHeader(height, coinbase.toAddress(), prevHash, timestamp, new byte[0], + new byte[0], new byte[0], data); // fetch pending transactions - final List pending = pendingMgr - .getPendingTransactions(config.spec().maxBlockTransactionsSize()); - final List pendingTxs = new ArrayList<>(); - final List pendingResults = new ArrayList<>(); + final List pendingTxs = pendingMgr + .getPendingTransactions(config.poolBlockGasLimit()); + final List includedTxs = new ArrayList<>(); + final List includedResults = new ArrayList<>(); - // for any VM requests, actually need to execute them AccountState as = accountState.track(); DelegateState ds = delegateState.track(); TransactionExecutor exec = new TransactionExecutor(config, blockStore); - BlockHeader tempHeader = new BlockHeader(height, coinbase.toAddress(), prevHash, timestamp, new byte[0], - new byte[0], new byte[0], data); + SemuxBlock semuxBlock = new SemuxBlock(tempHeader, config.spec().maxBlockGasLimit()); // only propose gas used up to configured block gas limit long remainingBlockGas = config.poolBlockGasLimit(); - SemuxBlock semuxBlock = new SemuxBlock(tempHeader, config.spec().maxBlockGasLimit()); - - for (PendingManager.PendingTransaction pendingTx : pending) { + for (PendingManager.PendingTransaction pendingTx : pendingTxs) { Transaction tx = pendingTx.transaction; - boolean isVMTransaction = tx.getType() == TransactionType.CALL || tx.getType() == TransactionType.CREATE; - if (isVMTransaction) { - // transactions that exceed the remaining block gas limit are ignored - if (tx.getGas() <= remainingBlockGas) { - TransactionResult result = exec.execute(tx, as, ds, semuxBlock, chain); - - // only include transaction that's acceptable - if (result.getCode().isAcceptable()) { - pendingResults.add(result); - pendingTxs.add(tx); + long gas = tx.isVMTransaction() ? tx.getGas() : config.spec().nonVMTransactionGasCost(); + if (gas > remainingBlockGas) { + break; + } - remainingBlockGas -= result.getGasUsed(); - } - } - } else { - if (config.spec().nonVMTransactionGasCost() <= remainingBlockGas - && pendingTx.result.getCode().isAcceptable()) { - pendingResults.add(pendingTx.result); - pendingTxs.add(pendingTx.transaction); - remainingBlockGas -= config.spec().nonVMTransactionGasCost(); - } + // re-evaluate the transaction + TransactionResult result = exec.execute(tx, as, ds, semuxBlock, chain, 0); + if (result.getCode().isAcceptable()) { + long gasUsed = tx.isVMTransaction() ? result.getGasUsed() : config.spec().nonVMTransactionGasCost(); + includedTxs.add(tx); + includedResults.add(result); + remainingBlockGas -= gasUsed; } } // compute roots - byte[] transactionsRoot = MerkleUtil.computeTransactionsRoot(pendingTxs); - byte[] resultsRoot = MerkleUtil.computeResultsRoot(pendingResults); + byte[] transactionsRoot = MerkleUtil.computeTransactionsRoot(includedTxs); + byte[] resultsRoot = MerkleUtil.computeResultsRoot(includedResults); byte[] stateRoot = Bytes.EMPTY_HASH; BlockHeader header = new BlockHeader(number, coinbase.toAddress(), prevHash, timestamp, transactionsRoot, resultsRoot, stateRoot, data); - Block block = new Block(header, pendingTxs, pendingResults); + Block block = new Block(header, includedTxs, includedResults); long t2 = TimeUtil.currentTimeMillis(); - logger.debug("Block creation: # txs = {}, time = {} ms", pendingTxs.size(), t2 - t1); + logger.debug("Block creation: # txs = {}, time = {} ms", includedTxs.size(), t2 - t1); return block; } /** - * Check if a block proposal is success. - * + * Check if a block proposal is valid. TODO: too much redundant code with + * {@link org.semux.core.BlockchainImpl#validateBlock(Block, AccountState, DelegateState, boolean)} + * . */ protected boolean validateBlockProposal(BlockHeader header, List transactions) { - Block block = new Block(header, transactions); - return validateBlockProposal(block); - } - - protected boolean validateBlockProposal(Block block) { - BlockHeader header = block.getHeader(); - List transactions = block.getTransactions(); - - long t1 = TimeUtil.currentTimeMillis(); + try { + Block block = new Block(header, transactions); + long t1 = TimeUtil.currentTimeMillis(); + + // [1] check block header + Block latest = chain.getLatestBlock(); + if (!block.validateHeader(header, latest.getHeader())) { + logger.warn("Invalid block header"); + return false; + } - // [1] check block header - Block latest = chain.getLatestBlock(); - if (!block.validateHeader(latest.getHeader(), header)) { - logger.warn("Invalid block header"); - return false; - } + if (header.getTimestamp() - TimeUtil.currentTimeMillis() > config.bftMaxBlockTimeDrift()) { + logger.warn("A block in the future is not allowed"); + return false; + } - if (header.getTimestamp() - TimeUtil.currentTimeMillis() > config.bftMaxBlockTimeDrift()) { - logger.warn("A block in the future is not allowed"); - return false; - } + if (Arrays.equals(header.getCoinbase(), Constants.COINBASE_ADDRESS)) { + logger.warn("A block forged by the coinbase magic account is not allowed"); + return false; + } - if (Arrays.equals(header.getCoinbase(), Constants.COINBASE_ADDRESS)) { - logger.warn("A block forged by the coinbase magic account is not allowed"); - return false; - } + if (!Arrays.equals(header.getCoinbase(), proposal.getSignature().getAddress())) { + logger.warn("The coinbase should always equal to the proposer's address"); + return false; + } - if (!Arrays.equals(header.getCoinbase(), proposal.getSignature().getAddress())) { - logger.warn("The coinbase should always equal to the proposer's address"); - return false; - } + // [2] check transactions and results (skipped) + List unvalidatedTransactions = getUnvalidatedTransactions(transactions); - // [2] check transactions and results (skipped) - List unvalidatedTransactions = getUnvalidatedTransactions(transactions); + if (!block.validateTransactions(header, unvalidatedTransactions, transactions, config.network())) { + logger.warn("Invalid block transactions"); + return false; + } - if (!block.validateTransactions(header, unvalidatedTransactions, transactions, config.network())) { - logger.warn("Invalid block transactions"); - return false; - } + if (transactions.stream().anyMatch(tx -> chain.hasTransaction(tx.getHash()))) { + logger.warn("Duplicated transaction hash is not allowed"); + return false; + } - if (transactions.stream().mapToInt(Transaction::size).sum() > config.spec().maxBlockTransactionsSize()) { - logger.warn("Block transactions size exceeds maximum"); - return false; - } + AccountState as = accountState.track(); + DelegateState ds = delegateState.track(); + TransactionExecutor exec = new TransactionExecutor(config, blockStore); - if (transactions.stream().anyMatch(tx -> chain.hasTransaction(tx.getHash()))) { - logger.warn("Duplicated transaction hash is not allowed"); - return false; - } + // [3] evaluate transactions + // When we are applying or validating block, we do not track transactions + // against our own local limit, only when proposing + List results = exec.execute(transactions, as, ds, + new SemuxBlock(header, config.spec().maxBlockGasLimit()), chain, 0); + block.setResults(results); - AccountState as = accountState.track(); - DelegateState ds = delegateState.track(); - TransactionExecutor exec = new TransactionExecutor(config, blockStore); + if (!block.validateResults(header, results)) { + logger.warn("Invalid transactions"); + return false; + } - // [3] evaluate transactions - // When we are applying or validating block, we do not track transactions - // against our own local limit, only - // when proposing - List results = exec.execute(transactions, as, ds, - new SemuxBlock(header, config.spec().maxBlockGasLimit()), chain); - block.setResults(results); + long t2 = TimeUtil.currentTimeMillis(); + logger.debug("Block validation: # txs = {}, time = {} ms", transactions.size(), t2 - t1); - if (!block.validateResults(header, results)) { - logger.warn("Invalid transactions"); + validBlocks.put(ByteArray.of(block.getHash()), block); + return true; + } catch (Exception e) { + logger.error("Unexpected exception during block proposal validation", e); return false; } - - long t2 = TimeUtil.currentTimeMillis(); - logger.debug("Block validation: # txs = {}, time = {} ms", transactions.size(), t2 - t1); - - validBlocks.put(ByteArray.of(block.getHash()), block); - return true; } /** @@ -931,70 +903,6 @@ protected List getUnvalidatedTransactions(List transac return unvalidatedTransactions; } - /** - * Apply a block to the chain. - * - * @param block - */ - protected void applyBlock(Block block) { - - long t1 = TimeUtil.currentTimeMillis(); - - BlockHeader header = block.getHeader(); - List transactions = block.getTransactions(); - long number = header.getNumber(); - - if (number != chain.getLatestBlockNumber() + 1) { - throw new SemuxBftException("Applying wrong block: number = " + number); - } - - // [1] check block header, skipped - - // [2] check transactions and results, skipped - - AccountState as = chain.getAccountState().track(); - DelegateState ds = chain.getDelegateState().track(); - TransactionExecutor exec = new TransactionExecutor(config, blockStore); - - // [3] evaluate all transactions - List results = exec.execute(transactions, as, ds, - new SemuxBlock(block.getHeader(), config.spec().maxBlockGasLimit()), chain); - if (!block.validateResults(header, results)) { - logger.debug("Invalid transactions"); - return; - } - - // [4] evaluate votes, skipped - - // [5] apply block reward and tx fees - Amount reward = Block.getBlockReward(block, config); - - if (reward.gt0()) { - as.adjustAvailable(block.getCoinbase(), reward); - } - - // [6] commit the updates - as.commit(); - ds.commit(); - - WriteLock lock = kernel.getStateLock().writeLock(); - lock.lock(); - try { - // [7] flush state to disk - chain.getAccountState().commit(); - chain.getDelegateState().commit(); - - // [8] add block to chain - chain.addBlock(block); - } finally { - lock.unlock(); - } - - long t2 = TimeUtil.currentTimeMillis(); - logger.debug("Block apply: # txs = {}, time = {} ms", transactions.size(), t2 - t1); - - } - public enum State { NEW_HEIGHT, PROPOSE, VALIDATE, PRE_COMMIT, COMMIT, FINALIZE } diff --git a/src/main/java/org/semux/consensus/SemuxSync.java b/src/main/java/org/semux/consensus/SemuxSync.java index ebccc43a0..bbfea62ff 100644 --- a/src/main/java/org/semux/consensus/SemuxSync.java +++ b/src/main/java/org/semux/consensus/SemuxSync.java @@ -13,16 +13,13 @@ import java.time.Duration; import java.time.Instant; import java.util.Arrays; -import java.util.Collections; import java.util.Comparator; import java.util.HashMap; -import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Random; -import java.util.Set; import java.util.TreeMap; import java.util.TreeSet; import java.util.concurrent.Executors; @@ -33,26 +30,15 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock; import java.util.stream.Collectors; import org.apache.commons.lang3.tuple.Pair; import org.ethereum.vm.client.BlockStore; import org.semux.Kernel; import org.semux.config.Config; -import org.semux.config.Constants; -import org.semux.core.Amount; import org.semux.core.Block; -import org.semux.core.BlockHeader; import org.semux.core.Blockchain; import org.semux.core.SyncManager; -import org.semux.core.Transaction; -import org.semux.core.TransactionExecutor; -import org.semux.core.TransactionResult; -import org.semux.core.state.AccountState; -import org.semux.core.state.DelegateState; -import org.semux.crypto.Hex; -import org.semux.crypto.Key; import org.semux.net.Channel; import org.semux.net.ChannelManager; import org.semux.net.msg.Message; @@ -60,9 +46,7 @@ import org.semux.net.msg.consensus.BlockMessage; import org.semux.net.msg.consensus.BlockPartsMessage; import org.semux.net.msg.consensus.GetBlockMessage; -import org.semux.util.ByteArray; import org.semux.util.TimeUtil; -import org.semux.vm.client.SemuxBlock; import org.semux.vm.client.SemuxBlockStore; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -428,22 +412,27 @@ protected void process() { // Validate and apply block to the chain if (pair != null) { - // If fastSync is true - skip vote validation - if (validateApplyBlock(pair.getKey(), !fastSync)) { + Block block = pair.getKey(); + boolean validateVotes = !fastSync; // If fastSync is true, skip vote validation + + if (chain.importBlock(block, validateVotes)) { + // update current height + current.set(block.getNumber() + 1); + synchronized (lock) { - if (toDownload.remove(pair.getKey().getNumber())) { + if (toDownload.remove(block.getNumber())) { growToDownloadQueue(); } - toComplete.remove(pair.getKey().getNumber()); - if (pair.getKey().getNumber() == lastBlockInSet) { - logger.info("{}", pair.getKey()); // Log last block in set + toComplete.remove(block.getNumber()); + if (block.getNumber() == lastBlockInSet) { + logger.info("{}", block); // Log last block in set fastSync = false; } else if (!fastSync) { - logger.info("{}", pair.getKey()); // Log all blocks + logger.info("{}", block); // Log all blocks } } } else { - handleInvalidBlock(pair.getKey(), pair.getValue()); + handleInvalidBlock(block, pair.getValue()); } } } @@ -469,12 +458,13 @@ protected void validateSetHashes() { } } else if (p.getKey().getNumber() == lastBlockInSet) { iterator.remove(); - // Validate votes for last block in set - if (validateBlockVotes(p.getKey()) && p.getKey().getHeader().validate()) { - toFinalize.put(p.getKey().getNumber(), p); - toComplete.remove(p.getKey().getNumber()); + + Block block = p.getKey(); // Validate votes for last block in set + if (chain.validateBlockVotes(block)) { + toFinalize.put(block.getNumber(), p); + toComplete.remove(block.getNumber()); } else { - handleInvalidBlock(p.getKey(), p.getValue()); + handleInvalidBlock(block, p.getValue()); return; } } else { @@ -506,173 +496,6 @@ protected void handleInvalidBlock(Block block, Channel channel) { channel.getMessageQueue().disconnect(ReasonCode.BAD_PEER); } - /** - * Check if a block is valid, and apply to the chain if yes. Votes are validated - * only if validateVotes is true. - * - * @param block - * @param validateVotes - * @return - */ - protected boolean validateApplyBlock(Block block, boolean validateVotes) { - AccountState as = chain.getAccountState().track(); - DelegateState ds = chain.getDelegateState().track(); - return validateBlock(block, as, ds, validateVotes) && applyBlock(block, as, ds); - } - - protected boolean validateApplyBlock(Block block) { - return validateApplyBlock(block, true); - } - - /** - * Validate the block. Votes are validated only if validateVotes is true. - * - * @param block - * @param asSnapshot - * @param dsSnapshot - * @param validateVotes - * @return - */ - protected boolean validateBlock(Block block, AccountState asSnapshot, DelegateState dsSnapshot, - boolean validateVotes) { - BlockHeader header = block.getHeader(); - List transactions = block.getTransactions(); - - // [1] check block header - Block latest = chain.getLatestBlock(); - if (!block.validateHeader(latest.getHeader(), header)) { - logger.error("Invalid block header"); - return false; - } - - // validate checkpoint - if (config.checkpoints().containsKey(header.getNumber()) && - !Arrays.equals(header.getHash(), config.checkpoints().get(header.getNumber()))) { - logger.error("Checkpoint validation failed, checkpoint is {} => {}, getting {}", header.getNumber(), - Hex.encode0x(config.checkpoints().get(header.getNumber())), - Hex.encode0x(header.getHash())); - return false; - } - - // blocks should never be forged by coinbase magic account - if (Arrays.equals(header.getCoinbase(), Constants.COINBASE_ADDRESS)) { - logger.error("A block forged by the coinbase magic account is not allowed"); - return false; - } - - // [2] check transactions and results - if (!block.validateTransactions(header, transactions, config.network()) - || transactions.stream().mapToInt(Transaction::size).sum() > config.spec().maxBlockTransactionsSize()) { - logger.error("Invalid block transactions"); - return false; - } - if (!block.validateResults(header, block.getResults())) { - logger.error("Invalid results"); - return false; - } - - if (transactions.stream().anyMatch(tx -> chain.hasTransaction(tx.getHash()))) { - logger.error("Duplicated transaction hash is not allowed"); - return false; - } - - // [3] evaluate transactions - TransactionExecutor transactionExecutor = new TransactionExecutor(config, blockStore); - List results = transactionExecutor.execute(transactions, asSnapshot, dsSnapshot, - new SemuxBlock(block.getHeader(), config.spec().maxBlockGasLimit()), chain); - block.setResults(results); - - if (!block.validateResults(header, results)) { - logger.error("Invalid transactions"); - return false; - } - - // [4] evaluate votes - if (validateVotes) { - return validateBlockVotes(block); - } - return true; - } - - protected boolean validateBlock(Block block, AccountState asSnapshot, DelegateState dsSnapshot) { - return validateBlock(block, asSnapshot, dsSnapshot, true); - } - - protected boolean validateBlockVotes(Block block) { - int maxValidators = config.spec().getNumberOfValidators(block.getNumber()); - - List validatorList = chain.getValidators(); - - if (validatorList.size() > maxValidators) { - validatorList = validatorList.subList(0, maxValidators); - } - Set validators = new HashSet<>(validatorList); - - int twoThirds = (int) Math.ceil(validators.size() * 2.0 / 3.0); - - Vote vote = new Vote(VoteType.PRECOMMIT, Vote.VALUE_APPROVE, block.getNumber(), block.getView(), - block.getHash()); - byte[] encoded = vote.getEncoded(); - - // check validity of votes - if (block.getVotes().stream().anyMatch(sig -> !validators.contains(Hex.encode(sig.getAddress())))) { - logger.warn("Block votes are invalid"); - return false; - } - - if (!Key.isVerifyBatchSupported()) { - if (!block.getVotes().stream() - .allMatch(sig -> Key.verify(encoded, sig))) { - logger.warn("Block votes are invalid"); - return false; - } - } else { - if (!Key.verifyBatch(Collections.nCopies(block.getVotes().size(), encoded), block.getVotes())) { - logger.warn("Block votes are invalid"); - return false; - } - } - - // at least two thirds voters - if (block.getVotes().stream() - .map(sig -> new ByteArray(sig.getA())) - .collect(Collectors.toSet()).size() < twoThirds) { - logger.warn("Not enough votes, needs 2/3+ twoThirds = {}, block = {}", twoThirds, block); - return false; - } - - return true; - } - - protected boolean applyBlock(Block block, AccountState asSnapshot, DelegateState dsSnapshot) { - // [5] apply block reward and tx fees - Amount reward = Block.getBlockReward(block, config); - - if (reward.gt0()) { - asSnapshot.adjustAvailable(block.getCoinbase(), reward); - } - - // [6] commit the updates - asSnapshot.commit(); - dsSnapshot.commit(); - - WriteLock writeLock = kernel.getStateLock().writeLock(); - writeLock.lock(); - try { - // [7] flush state to disk - chain.getAccountState().commit(); - chain.getDelegateState().commit(); - - // [8] add block to chain - chain.addBlock(block); - } finally { - writeLock.unlock(); - } - - current.set(block.getNumber() + 1); - return true; - } - @Override public SemuxSyncProgress getProgress() { return new SemuxSyncProgress( diff --git a/src/main/java/org/semux/core/Block.java b/src/main/java/org/semux/core/Block.java index 3f7723e2a..952653056 100644 --- a/src/main/java/org/semux/core/Block.java +++ b/src/main/java/org/semux/core/Block.java @@ -139,10 +139,10 @@ public void setVotes(List votes) { * Validates block header. * * @param header - * @param previous + * @param parentHeader * @return */ - public boolean validateHeader(BlockHeader previous, BlockHeader header) { + public boolean validateHeader(BlockHeader header, BlockHeader parentHeader) { if (header == null) { logger.warn("Header was null."); return false; @@ -153,17 +153,17 @@ public boolean validateHeader(BlockHeader previous, BlockHeader header) { return false; } - if (header.getNumber() != previous.getNumber() + 1) { + if (header.getNumber() != parentHeader.getNumber() + 1) { logger.warn("Header number was not one greater than previous block."); return false; } - if (!Arrays.equals(header.getParentHash(), previous.getHash())) { + if (!Arrays.equals(header.getParentHash(), parentHeader.getHash())) { logger.warn("Header parent hash was not equal to previous block hash."); return false; } - if (header.getTimestamp() <= previous.getTimestamp()) { + if (header.getTimestamp() <= parentHeader.getTimestamp()) { logger.warn("Header timestamp was before previous block."); return false; } diff --git a/src/main/java/org/semux/core/Blockchain.java b/src/main/java/org/semux/core/Blockchain.java index af6b9a54c..9a4fb73ec 100644 --- a/src/main/java/org/semux/core/Blockchain.java +++ b/src/main/java/org/semux/core/Blockchain.java @@ -7,6 +7,7 @@ package org.semux.core; import java.util.List; +import java.util.concurrent.locks.ReentrantReadWriteLock; import org.semux.core.BlockchainImpl.ValidatorStats; import org.semux.core.state.AccountState; @@ -212,5 +213,35 @@ public interface Blockchain { */ boolean isForkActivated(Fork fork); - byte[] constructBlockData(); + /** + * Returns the date field for the next block, based on fork configuration. + * + * @return + */ + byte[] constructBlockHeaderDataField(); + + /** + * Returns the state lock. + * + * @return + */ + ReentrantReadWriteLock getStateLock(); + + /** + * Imports a new block. + * + * @param block + * the block to import + * @param validateVotes + * whether to validate the block votes + * @return true if the block is successfully imported; otherwise, false + */ + boolean importBlock(Block block, boolean validateVotes); + + /** + * Validate the block votes only. + * + * @return true if the votes are valid, otherwise false + */ + boolean validateBlockVotes(Block block); } diff --git a/src/main/java/org/semux/core/BlockchainImpl.java b/src/main/java/org/semux/core/BlockchainImpl.java index 24eb84101..cc8eae48f 100644 --- a/src/main/java/org/semux/core/BlockchainImpl.java +++ b/src/main/java/org/semux/core/BlockchainImpl.java @@ -13,17 +13,22 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.stream.Collectors; import org.apache.commons.lang3.tuple.Pair; import org.ethereum.vm.client.BlockStore; import org.semux.config.Config; import org.semux.config.Constants; +import org.semux.consensus.Vote; +import org.semux.consensus.VoteType; import org.semux.core.Genesis.Premine; import org.semux.core.event.BlockchainDatabaseUpgradingEvent; import org.semux.core.exception.BlockchainException; @@ -33,6 +38,7 @@ import org.semux.core.state.DelegateState; import org.semux.core.state.DelegateStateImpl; import org.semux.crypto.Hex; +import org.semux.crypto.Key; import org.semux.db.Database; import org.semux.db.DatabaseFactory; import org.semux.db.DatabaseName; @@ -40,6 +46,7 @@ import org.semux.db.Migration; import org.semux.event.PubSub; import org.semux.event.PubSubFactory; +import org.semux.util.ByteArray; import org.semux.util.Bytes; import org.semux.util.SimpleDecoder; import org.semux.util.SimpleEncoder; @@ -97,7 +104,8 @@ public class BlockchainImpl implements Blockchain { protected static final byte TYPE_BLOCK_RESULTS = 0x02; protected static final byte TYPE_BLOCK_VOTES = 0x03; - private BlockStore blockStore = new SemuxBlockStore(this); + private final BlockStore blockStore = new SemuxBlockStore(this); + private final ReentrantReadWriteLock stateLock = new ReentrantReadWriteLock(); protected enum StatsType { FORGED, HIT, MISSED @@ -620,7 +628,7 @@ public boolean isForkActivated(Fork fork, long height) { } @Override - public byte[] constructBlockData() { + public byte[] constructBlockHeaderDataField() { Set set = new HashSet<>(); if (config.forkUniformDistributionEnabled() && !forks.isActivated(UNIFORM_DISTRIBUTION) @@ -638,6 +646,168 @@ public byte[] constructBlockData() { : BlockHeaderData.v1(new BlockHeaderData.ForkSignalSet(set.toArray(new Fork[0]))).toBytes(); } + @Override + public ReentrantReadWriteLock getStateLock() { + return stateLock; + } + + @Override + public boolean importBlock(Block block, boolean validateVotes) { + AccountState as = this.getAccountState().track(); + DelegateState ds = this.getDelegateState().track(); + return validateBlock(block, as, ds, validateVotes) && applyBlock(block, as, ds); + } + + /** + * Validate the block. Votes are validated only if validateVotes is true. + * + * @param block + * @param asSnapshot + * @param dsSnapshot + * @param validateVotes + * @return + */ + protected boolean validateBlock(Block block, AccountState asSnapshot, DelegateState dsSnapshot, + boolean validateVotes) { + try { + BlockHeader header = block.getHeader(); + List transactions = block.getTransactions(); + + // [1] check block header + Block latest = this.getLatestBlock(); + if (!block.validateHeader(header, latest.getHeader())) { + logger.error("Invalid block header"); + return false; + } + + // validate checkpoint + if (config.checkpoints().containsKey(header.getNumber()) && + !Arrays.equals(header.getHash(), config.checkpoints().get(header.getNumber()))) { + logger.error("Checkpoint validation failed, checkpoint is {} => {}, getting {}", header.getNumber(), + Hex.encode0x(config.checkpoints().get(header.getNumber())), + Hex.encode0x(header.getHash())); + return false; + } + + // blocks should never be forged by coinbase magic account + if (Arrays.equals(header.getCoinbase(), Constants.COINBASE_ADDRESS)) { + logger.error("A block forged by the coinbase magic account is not allowed"); + return false; + } + + // [2] check transactions and results + if (!block.validateTransactions(header, transactions, config.network())) { + logger.error("Invalid block transactions"); + return false; + } + if (!block.validateResults(header, block.getResults())) { + logger.error("Invalid results"); + return false; + } + + if (transactions.stream().anyMatch(tx -> this.hasTransaction(tx.getHash()))) { + logger.error("Duplicated transaction hash is not allowed"); + return false; + } + + // [3] evaluate transactions + TransactionExecutor transactionExecutor = new TransactionExecutor(config, blockStore); + List results = transactionExecutor.execute(transactions, asSnapshot, dsSnapshot, + new SemuxBlock(block.getHeader(), config.spec().maxBlockGasLimit()), this, 0); + block.setResults(results); + + if (!block.validateResults(header, results)) { + logger.error("Invalid transactions"); + return false; + } + + // [4] evaluate votes + if (validateVotes) { + return validateBlockVotes(block); + } + + return true; + } catch (Exception e) { + logger.error("Unexpected exception during block validation", e); + return false; + } + } + + @Override + public boolean validateBlockVotes(Block block) { + int maxValidators = config.spec().getNumberOfValidators(block.getNumber()); + + List validatorList = this.getValidators(); + + if (validatorList.size() > maxValidators) { + validatorList = validatorList.subList(0, maxValidators); + } + Set validators = new HashSet<>(validatorList); + + int twoThirds = (int) Math.ceil(validators.size() * 2.0 / 3.0); + + Vote vote = new Vote(VoteType.PRECOMMIT, Vote.VALUE_APPROVE, block.getNumber(), block.getView(), + block.getHash()); + byte[] encoded = vote.getEncoded(); + + // check validity of votes + if (block.getVotes().stream().anyMatch(sig -> !validators.contains(Hex.encode(sig.getAddress())))) { + logger.warn("Block votes are invalid"); + return false; + } + + if (!Key.isVerifyBatchSupported()) { + if (!block.getVotes().stream() + .allMatch(sig -> Key.verify(encoded, sig))) { + logger.warn("Block votes are invalid"); + return false; + } + } else { + if (!Key.verifyBatch(Collections.nCopies(block.getVotes().size(), encoded), block.getVotes())) { + logger.warn("Block votes are invalid"); + return false; + } + } + + // at least two thirds voters + if (block.getVotes().stream() + .map(sig -> new ByteArray(sig.getA())) + .collect(Collectors.toSet()).size() < twoThirds) { + logger.warn("Not enough votes, needs 2/3+ twoThirds = {}, block = {}", twoThirds, block); + return false; + } + + return true; + } + + protected boolean applyBlock(Block block, AccountState asSnapshot, DelegateState dsSnapshot) { + // [5] apply block reward and tx fees + Amount reward = Block.getBlockReward(block, config); + + if (reward.gt0()) { + asSnapshot.adjustAvailable(block.getCoinbase(), reward); + } + + // [6] commit the updates + asSnapshot.commit(); + dsSnapshot.commit(); + + ReentrantReadWriteLock.WriteLock writeLock = this.stateLock.writeLock(); + writeLock.lock(); + try { + // [7] flush state to disk + this.getAccountState().commit(); + this.getDelegateState().commit(); + + // [8] add block to chain + this.addBlock(block); + } finally { + writeLock.unlock(); + } + + return true; + } + /** * Attempt to activate pending forks at current height. */ @@ -700,9 +870,8 @@ private void upgradeDb0(DatabaseFactory dbFactory) { /** * A temporary blockchain for database migration. This class implements a - * lightweight version of - * ${@link org.semux.consensus.SemuxBft#applyBlock(Block)} to migrate blocks - * from an existing database to the latest schema. + * lightweight version of ${@link org.semux.core.Blockchain#importBlock(Block)} + * to migrate blocks from an existing database to the latest schema. */ private class MigrationBlockchain extends BlockchainImpl { private MigrationBlockchain(Config config, DatabaseFactory dbFactory) { @@ -713,7 +882,7 @@ public void applyBlock(Block block) { // [0] execute transactions against local state TransactionExecutor transactionExecutor = new TransactionExecutor(config, blockStore); transactionExecutor.execute(block.getTransactions(), getAccountState(), getDelegateState(), - new SemuxBlock(block.getHeader(), config.spec().maxBlockGasLimit()), this); + new SemuxBlock(block.getHeader(), config.spec().maxBlockGasLimit()), this, 0); // [1] apply block reward and tx fees Amount reward = Block.getBlockReward(block, config); diff --git a/src/main/java/org/semux/core/PendingManager.java b/src/main/java/org/semux/core/PendingManager.java index f43da63ee..73228e6df 100644 --- a/src/main/java/org/semux/core/PendingManager.java +++ b/src/main/java/org/semux/core/PendingManager.java @@ -41,8 +41,12 @@ * Pending manager maintains all unconfirmed transactions, either from kernel or * network. All transactions are evaluated and propagated to peers if success. * - * TODO: sort transaction queue by fee, and other metrics + * Note that: the transaction results in pending manager are not reliable for VM + * transactions because these are evaluated against a dummy block. Nevertheless, + * transactions included by the pending manager are eligible for inclusion in + * block proposing phase. * + * TODO: sort transaction queue by fee, and other metrics */ public class PendingManager implements Runnable, BlockchainListener { @@ -69,6 +73,7 @@ public Thread newThread(Runnable r) { private final BlockStore blockStore; private AccountState pendingAS; private DelegateState pendingDS; + private SemuxBlock dummyBlock; /** * Transaction queue. @@ -103,6 +108,7 @@ public PendingManager(Kernel kernel) { this.pendingAS = kernel.getBlockchain().getAccountState().track(); this.pendingDS = kernel.getBlockchain().getDelegateState().track(); + this.dummyBlock = createDummyBlock(); this.exec = Executors.newSingleThreadScheduledExecutor(factory); } @@ -205,26 +211,21 @@ public synchronized long getNonce(byte[] address) { /** * Returns pending transactions, limited by the given total size in bytes. * - * @param byteLimit + * * @return */ - public synchronized List getPendingTransactions(int byteLimit) { - if (byteLimit < 0) { - throw new IllegalArgumentException("Limit can't be negative"); - } - + public synchronized List getPendingTransactions(long blockGasLimit) { List txs = new ArrayList<>(); Iterator it = transactions.iterator(); - int size = 0; - while (it.hasNext()) { + while (it.hasNext() && blockGasLimit > 0) { PendingTransaction tx = it.next(); - size += tx.transaction.size(); - if (size > byteLimit) { - break; - } else { + long gasUsage = tx.transaction.isVMTransaction() ? tx.result.getGasUsed() + : kernel.getConfig().spec().nonVMTransactionGasCost(); + if (blockGasLimit > gasUsage) { txs.add(tx); + blockGasLimit -= gasUsage; } } @@ -237,7 +238,7 @@ public synchronized List getPendingTransactions(int byteLimi * @return */ public synchronized List getPendingTransactions() { - return getPendingTransactions(Integer.MAX_VALUE); + return getPendingTransactions(Long.MAX_VALUE); } /** @@ -249,6 +250,7 @@ public synchronized List reset() { // reset state pendingAS = kernel.getBlockchain().getAccountState().track(); pendingDS = kernel.getBlockchain().getDelegateState().track(); + dummyBlock = createDummyBlock(); // clear transaction pool List txs = new ArrayList<>(transactions); @@ -314,14 +316,13 @@ protected ProcessingResult processTransaction(Transaction tx, boolean relay) { int cnt = 0; long now = TimeUtil.currentTimeMillis(); - boolean isVMTransaction = tx.getType() == TransactionType.CALL || tx.getType() == TransactionType.CREATE; // reject VM transactions that come in before fork - if (isVMTransaction && !kernel.getBlockchain().isForkActivated(Fork.VIRTUAL_MACHINE)) { + if (tx.isVMTransaction() && !kernel.getBlockchain().isForkActivated(Fork.VIRTUAL_MACHINE)) { return new ProcessingResult(0, TransactionResult.Code.INVALID_TYPE); } // reject VM transaction with low gas price - if (isVMTransaction && tx.getGasPrice().lt(kernel.getConfig().poolMinGasPrice())) { + if (tx.isVMTransaction() && tx.getGasPrice().lt(kernel.getConfig().poolMinGasPrice())) { return new ProcessingResult(0, TransactionResult.Code.INVALID_FEE); } @@ -349,22 +350,11 @@ protected ProcessingResult processTransaction(Transaction tx, boolean relay) { // delayed for the next event loop of PendingManager. while (tx != null && tx.getNonce() == getNonce(tx.getFrom())) { - // TODO: introduce block state (open, closed, signed, imported) - - // create a dummy block (Note: VM transaction results may depends on the block) - Blockchain chain = kernel.getBlockchain(); - Block prevBlock = chain.getLatestBlock(); - BlockHeader blockHeader = new BlockHeader( - prevBlock.getNumber() + 1, - new Key().toAddress(), prevBlock.getHash(), System.currentTimeMillis(), new byte[0], - new byte[0], new byte[0], new byte[0]); - SemuxBlock block = new SemuxBlock(blockHeader, kernel.getConfig().spec().maxBlockGasLimit()); - // execute transactions AccountState as = pendingAS.track(); DelegateState ds = pendingDS.track(); TransactionResult result = new TransactionExecutor(kernel.getConfig(), blockStore).execute(tx, - as, ds, block, chain); + as, ds, dummyBlock, kernel.getBlockchain(), 0); if (result.getCode().isAcceptable()) { // commit state updates @@ -415,6 +405,16 @@ private ByteArray createKey(byte[] acc, long nonce) { return ByteArray.of(Bytes.merge(acc, Bytes.of(nonce))); } + private SemuxBlock createDummyBlock() { + Blockchain chain = kernel.getBlockchain(); + Block prevBlock = chain.getLatestBlock(); + BlockHeader blockHeader = new BlockHeader( + prevBlock.getNumber() + 1, + new Key().toAddress(), prevBlock.getHash(), System.currentTimeMillis(), Bytes.EMPTY_BYTES, + Bytes.EMPTY_BYTES, Bytes.EMPTY_BYTES, Bytes.EMPTY_BYTES); + return new SemuxBlock(blockHeader, kernel.getConfig().spec().maxBlockGasLimit()); + } + /** * This object represents a transaction and its execution result against a * snapshot of local state that is not yet confirmed by the network. diff --git a/src/main/java/org/semux/core/Transaction.java b/src/main/java/org/semux/core/Transaction.java index 9abd3a900..5df063747 100644 --- a/src/main/java/org/semux/core/Transaction.java +++ b/src/main/java/org/semux/core/Transaction.java @@ -125,6 +125,10 @@ public Transaction(Network network, TransactionType type, byte[] toAddress, Amou this(network, type, toAddress, value, fee, nonce, timestamp, data, 0, Amount.ZERO); } + public boolean isVMTransaction() { + return type == TransactionType.CREATE || type == TransactionType.CALL; + } + /** * Sign this transaction. * diff --git a/src/main/java/org/semux/core/TransactionExecutor.java b/src/main/java/org/semux/core/TransactionExecutor.java index 84199b886..6be8817b7 100644 --- a/src/main/java/org/semux/core/TransactionExecutor.java +++ b/src/main/java/org/semux/core/TransactionExecutor.java @@ -39,6 +39,7 @@ public class TransactionExecutor { private static final boolean[] valid = new boolean[256]; + static { for (byte b : Bytes.of("abcdefghijklmnopqrstuvwxyz0123456789_")) { valid[b & 0xff] = true; @@ -89,13 +90,17 @@ public TransactionExecutor(Config config, BlockStore blockStore) { * account state * @param ds * delegate state + * @param block + * the block context + * @param gasUsedInBlock + * the amount of gas that has been consumed by previous transaction + * in the block * @return */ public List execute(List txs, AccountState as, DelegateState ds, - SemuxBlock block, Blockchain chain) { + SemuxBlock block, Blockchain chain, long gasUsedInBlock) { List results = new ArrayList<>(); - long gasUsedInBlock = 0; for (Transaction tx : txs) { TransactionResult result = new TransactionResult(); results.add(result); @@ -118,16 +123,12 @@ public List execute(List txs, AccountState as, D continue; } - boolean isVMTransaction = type == TransactionType.CREATE || type == TransactionType.CALL; - - // check fee (call and create use gas instead) - if (isVMTransaction) { + // check fee (CREATE and CALL use gas instead) + if (tx.isVMTransaction()) { // applying a very strict check to avoid mistakes boolean valid = fee.equals(Amount.ZERO) && tx.getGas() >= 21_000 && tx.getGas() <= config.spec().maxBlockGasLimit() - && tx.getGasPrice().getNano() >= 1 && tx.getGasPrice().getNano() <= Integer.MAX_VALUE; // a - // theoretical - // limit + && tx.getGasPrice().getNano() >= 1 && tx.getGasPrice().getNano() <= Integer.MAX_VALUE; if (!valid) { result.setCode(Code.INVALID_FEE); continue; @@ -145,6 +146,17 @@ public List execute(List txs, AccountState as, D continue; } + // check remaining gas + if (!tx.isVMTransaction()) { + if (config.spec().nonVMTransactionGasCost() + gasUsedInBlock > block.getGasLimit()) { + result.setCode(Code.INVALID); + continue; + } + + // Note: although we count gas usage for non-vm-transactions, the gas usage + // is not recorded in the TransactionResult. + } + switch (type) { case TRANSFER: { if (fee.lte(available) && value.lte(available) && sum(value, fee).lte(available)) { @@ -222,8 +234,13 @@ public List execute(List txs, AccountState as, D // the VM transaction executor will check balance and gas cost. // do proper refunds afterwards. - executeVmTransaction(result, tx, as, ds, block, gasUsedInBlock); + + // Note: we're assuming the VM will not make changes to the account + // and delegate state if the transaction is INVALID; the storage changes + // will be discarded if is FAILURE. + // + // TODO: add unit test for this break; default: // unsupported transaction type @@ -232,12 +249,12 @@ public List execute(List txs, AccountState as, D } if (result.getCode().isAcceptable()) { - if (!isVMTransaction) { + if (!tx.isVMTransaction()) { // CREATEs and CALLs manages the nonce inside the VM as.increaseNonce(from); } - if (isVMTransaction) { + if (tx.isVMTransaction()) { gasUsedInBlock += result.getGasUsed(); } else { gasUsedInBlock += config.spec().nonVMTransactionGasCost(); @@ -292,10 +309,15 @@ private void executeVmTransaction(TransactionResult result, Transaction tx, Acco * delegate state * @param chain * the blockchain instance + * @param block + * the block context + * @param gasUsedInBlock + * the amount of gas that has been consumed by previous transaction + * in the block * @return */ public TransactionResult execute(Transaction tx, AccountState as, DelegateState ds, SemuxBlock block, - Blockchain chain) { - return execute(Collections.singletonList(tx), as, ds, block, chain).get(0); + Blockchain chain, long gasUsedInBlock) { + return execute(Collections.singletonList(tx), as, ds, block, chain, gasUsedInBlock).get(0); } } diff --git a/src/main/java/org/semux/net/msg/consensus/NewViewMessage.java b/src/main/java/org/semux/net/msg/consensus/NewViewMessage.java index 636d5bfda..882b641a2 100644 --- a/src/main/java/org/semux/net/msg/consensus/NewViewMessage.java +++ b/src/main/java/org/semux/net/msg/consensus/NewViewMessage.java @@ -19,7 +19,7 @@ public NewViewMessage(Proof proof) { this.proof = proof; - // FIXME: consider wrapping by simple codec + // TODO: consider wrapping by simple codec this.body = proof.toBytes(); } diff --git a/src/main/java/org/semux/net/msg/consensus/ProposalMessage.java b/src/main/java/org/semux/net/msg/consensus/ProposalMessage.java index 02cc229d1..bbaea20b0 100644 --- a/src/main/java/org/semux/net/msg/consensus/ProposalMessage.java +++ b/src/main/java/org/semux/net/msg/consensus/ProposalMessage.java @@ -19,7 +19,7 @@ public ProposalMessage(Proposal proposal) { this.proposal = proposal; - // FIXME: consider wrapping by simple codec + // TODO: consider wrapping by simple codec this.body = proposal.toBytes(); } diff --git a/src/main/java/org/semux/net/msg/consensus/VoteMessage.java b/src/main/java/org/semux/net/msg/consensus/VoteMessage.java index b53eee1b5..be51d5928 100644 --- a/src/main/java/org/semux/net/msg/consensus/VoteMessage.java +++ b/src/main/java/org/semux/net/msg/consensus/VoteMessage.java @@ -19,7 +19,7 @@ public VoteMessage(Vote vote) { this.vote = vote; - // FIXME: consider wrapping by simple codec + // TODO: consider wrapping by simple codec this.body = vote.toBytes(); } diff --git a/src/main/java/org/semux/net/msg/p2p/TransactionMessage.java b/src/main/java/org/semux/net/msg/p2p/TransactionMessage.java index ad5991093..c434783e3 100644 --- a/src/main/java/org/semux/net/msg/p2p/TransactionMessage.java +++ b/src/main/java/org/semux/net/msg/p2p/TransactionMessage.java @@ -23,7 +23,7 @@ public TransactionMessage(Transaction transaction) { this.transaction = transaction; - // FIXME: consider wrapping by simple codec + // TODO: consider wrapping by simple codec this.body = transaction.toBytes(); } diff --git a/src/main/java/org/semux/vm/client/SemuxPrecompiledContracts.java b/src/main/java/org/semux/vm/client/SemuxPrecompiledContracts.java index 585293d5e..cd11222c9 100644 --- a/src/main/java/org/semux/vm/client/SemuxPrecompiledContracts.java +++ b/src/main/java/org/semux/vm/client/SemuxPrecompiledContracts.java @@ -30,8 +30,8 @@ public class SemuxPrecompiledContracts extends ByzantiumPrecompiledContracts { private static final DataWord voteAddr = DataWord.of(100); private static final DataWord unvoteAddr = DataWord.of(101); - private static final Pair success = Pair.of(true, ArrayUtils.EMPTY_BYTE_ARRAY); - private static final Pair failure = Pair.of(false, ArrayUtils.EMPTY_BYTE_ARRAY); + private static final Pair success = new Pair<>(true, ArrayUtils.EMPTY_BYTE_ARRAY); + private static final Pair failure = new Pair<>(false, ArrayUtils.EMPTY_BYTE_ARRAY); @Override public PrecompiledContract getContractForAddress(DataWord address) { diff --git a/src/test/java/org/semux/api/v2/SemuxApiTest.java b/src/test/java/org/semux/api/v2/SemuxApiTest.java index 5f231bff8..bc2806a8f 100644 --- a/src/test/java/org/semux/api/v2/SemuxApiTest.java +++ b/src/test/java/org/semux/api/v2/SemuxApiTest.java @@ -632,7 +632,6 @@ public void transferTest() throws InterruptedException { String to = key.toAddressString(); String fee = "5432100"; String nonce = null; - Boolean validateNonce = null; String data = Hex.encode(Bytes.of("test_transfer")); DoTransactionResponse response = api.transfer(from, to, value, fee, nonce, data); diff --git a/src/test/java/org/semux/bench/BlockchainPerformance.java b/src/test/java/org/semux/bench/BlockchainPerformance.java index 59184e619..c94b920a5 100644 --- a/src/test/java/org/semux/bench/BlockchainPerformance.java +++ b/src/test/java/org/semux/bench/BlockchainPerformance.java @@ -46,8 +46,8 @@ public static Block testBlockCreation() { List txs = new ArrayList<>(); List res = new ArrayList<>(); - int total = 0; - for (int i = 0;; i++) { + long remainingBlockGas = config.spec().maxBlockGasLimit(); + for (int i = 0; remainingBlockGas >= config.spec().nonVMTransactionGasCost(); i++) { Network network = config.network(); TransactionType type = TransactionType.TRANSFER; byte[] to = Bytes.random(20); @@ -57,14 +57,10 @@ public static Block testBlockCreation() { long timestamp = TimeUtil.currentTimeMillis(); byte[] data = Bytes.EMPTY_BYTES; Transaction tx = new Transaction(network, type, to, value, fee, nonce, timestamp, data).sign(key); - - if (total + tx.size() > config.spec().maxBlockTransactionsSize()) { - break; - } - txs.add(tx); res.add(new TransactionResult()); - total += tx.size(); + + remainingBlockGas -= config.spec().nonVMTransactionGasCost(); } long number = 1; @@ -99,12 +95,12 @@ public static Block testBlockCreation() { } public static void testBlockValidation(Block block) { - Genesis gen = Genesis.load(Network.DEVNET); + Genesis genesis = Genesis.load(Network.DEVNET); long t1 = System.nanoTime(); - block.validateHeader(gen.getHeader(), block.getHeader()); - block.validateTransactions(gen.getHeader(), block.getTransactions(), config.network()); - block.validateResults(gen.getHeader(), block.getResults()); + block.validateHeader(block.getHeader(), genesis.getHeader()); + block.validateTransactions(block.getHeader(), block.getTransactions(), config.network()); + block.validateResults(block.getHeader(), block.getResults()); // block votes validation skipped long t2 = System.nanoTime(); diff --git a/src/test/java/org/semux/consensus/SemuxBftValidateBlockTest.java b/src/test/java/org/semux/consensus/SemuxBftValidateBlockTest.java index cfc8c69c3..c048e0671 100644 --- a/src/test/java/org/semux/consensus/SemuxBftValidateBlockTest.java +++ b/src/test/java/org/semux/consensus/SemuxBftValidateBlockTest.java @@ -40,9 +40,7 @@ public static Collection data() { return Arrays.asList(new Object[][] { { "block in the future", - (Callable) () -> { - return null; - }, + (Callable) () -> null, (Supplier) () -> { Blockchain blockchain = mock(Blockchain.class); when(blockchain.getLatestBlock()).thenReturn(mock(Block.class)); diff --git a/src/test/java/org/semux/consensus/SemuxSyncTest.java b/src/test/java/org/semux/consensus/SemuxSyncTest.java index 5dfc2ff35..b22a09502 100644 --- a/src/test/java/org/semux/consensus/SemuxSyncTest.java +++ b/src/test/java/org/semux/consensus/SemuxSyncTest.java @@ -8,45 +8,31 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; -import static org.semux.core.Amount.Unit.SEM; import java.net.InetSocketAddress; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.Comparator; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.TreeSet; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; -import org.apache.commons.lang3.RandomUtils; import org.apache.commons.lang3.tuple.Pair; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.junit.MockitoJUnitRunner; import org.semux.TestUtils; -import org.semux.config.Config; -import org.semux.config.Constants; -import org.semux.core.Amount; import org.semux.core.Block; import org.semux.core.BlockchainImpl; -import org.semux.core.Fork; -import org.semux.core.Transaction; -import org.semux.core.TransactionResult; -import org.semux.core.TransactionType; -import org.semux.core.state.AccountState; -import org.semux.core.state.DelegateState; import org.semux.crypto.Hex; import org.semux.crypto.Key; import org.semux.crypto.Key.Signature; @@ -54,8 +40,6 @@ import org.semux.net.msg.MessageQueue; import org.semux.rules.KernelRule; import org.semux.rules.TemporaryDatabaseRule; -import org.semux.util.Bytes; -import org.semux.util.TimeUtil; @RunWith(MockitoJUnitRunner.Silent.class) public class SemuxSyncTest { @@ -68,158 +52,6 @@ public class SemuxSyncTest { public long validatorInterval; - @Test - public void testDuplicatedTransaction() { - // mock blockchain with a single transaction - Key to = new Key(); - Key from1 = new Key(); - long time = TimeUtil.currentTimeMillis(); - Transaction tx1 = new Transaction( - kernelRule.getKernel().getConfig().network(), - TransactionType.TRANSFER, - to.toAddress(), - SEM.of(10), - kernelRule.getKernel().getConfig().spec().minTransactionFee(), - 0, - time, - Bytes.EMPTY_BYTES).sign(from1); - kernelRule.getKernel().setBlockchain(new BlockchainImpl(kernelRule.getKernel().getConfig(), temporaryDBRule)); - kernelRule.getKernel().getBlockchain().getAccountState().adjustAvailable(from1.toAddress(), SEM.of(1000)); - Block block1 = kernelRule.createBlock(Collections.singletonList(tx1)); - kernelRule.getKernel().getBlockchain().addBlock(block1); - SemuxSync semuxSync = spy(new SemuxSync(kernelRule.getKernel())); - doReturn(true).when(semuxSync).validateBlockVotes(any()); // we don't care about votes here - - // create a tx with the same hash with tx1 from a different signer in the second - // block - Key from2 = new Key(); - kernelRule.getKernel().getBlockchain().getAccountState().adjustAvailable(from2.toAddress(), SEM.of(1000)); - Transaction tx2 = new Transaction( - kernelRule.getKernel().getConfig().network(), - TransactionType.TRANSFER, - to.toAddress(), - SEM.of(10), - kernelRule.getKernel().getConfig().spec().minTransactionFee(), - 0, - time, - Bytes.EMPTY_BYTES).sign(from2); - Block block2 = kernelRule.createBlock(Collections.singletonList(tx2)); - - // this test case is valid if and only if tx1 and tx2 have the same tx hash - assert (Arrays.equals(tx1.getHash(), tx2.getHash())); - - // the block should be rejected because of the duplicated tx - AccountState as = kernelRule.getKernel().getBlockchain().getAccountState().track(); - DelegateState ds = kernelRule.getKernel().getBlockchain().getDelegateState().track(); - assertFalse(semuxSync.validateBlock(block2, as, ds)); - } - - @Test - public void testValidateCoinbaseMagic() { - BlockchainImpl blockchain = spy(new BlockchainImpl(kernelRule.getKernel().getConfig(), temporaryDBRule)); - kernelRule.enableForks(Fork.UNIFORM_DISTRIBUTION); - kernelRule.getKernel().setBlockchain(blockchain); - - // block.coinbase = coinbase magic account - Block block = TestUtils.createBlock( - kernelRule.getKernel().getBlockchain().getLatestBlock().getHash(), - Constants.COINBASE_KEY, - kernelRule.getKernel().getBlockchain().getLatestBlockNumber() + 1, - Collections.emptyList(), - Collections.emptyList()); - - AccountState as = kernelRule.getKernel().getBlockchain().getAccountState().track(); - DelegateState ds = kernelRule.getKernel().getBlockchain().getDelegateState().track(); - SemuxSync semuxSync = new SemuxSync(kernelRule.getKernel()); - assertFalse(semuxSync.validateBlock(block, as, ds)); - - // tx.to = coinbase magic account - Transaction tx = TestUtils.createTransaction(kernelRule.getKernel().getConfig(), new Key(), - Constants.COINBASE_KEY, Amount.ZERO); - Block block2 = TestUtils.createBlock( - kernelRule.getKernel().getBlockchain().getLatestBlock().getHash(), - new Key(), - kernelRule.getKernel().getBlockchain().getLatestBlockNumber() + 1, - Collections.singletonList(tx), - Collections.singletonList(new TransactionResult())); - - assertFalse(semuxSync.validateBlock(block2, as, ds)); - } - - @Test - public void testValidateBlockVotes() { - Key key1 = new Key(); - Key key2 = new Key(); - Key key3 = new Key(); - List validators = Arrays.asList(Hex.encode(key1.toAddress()), - Hex.encode(key2.toAddress()), - Hex.encode(key3.toAddress())); - - // mock the chain - BlockchainImpl chain = spy(new BlockchainImpl(kernelRule.getKernel().getConfig(), temporaryDBRule)); - doReturn(validators).when(chain).getValidators(); - kernelRule.getKernel().setBlockchain(chain); - - // mock sync manager - SemuxSync sync = spy(new SemuxSync(kernelRule.getKernel())); - - // prepare block - Block block = kernelRule.createBlock(Collections.emptyList()); - Vote vote = new Vote(VoteType.PRECOMMIT, Vote.VALUE_APPROVE, block.getNumber(), block.getView(), - block.getHash()); - byte[] encoded = vote.getEncoded(); - List votes = new ArrayList<>(); - block.setVotes(votes); - - // tests - assertFalse(sync.validateBlockVotes(block)); - - votes.add(key1.sign(encoded)); - votes.add(key1.sign(encoded)); - assertFalse(sync.validateBlockVotes(block)); - - votes.add(key2.sign(encoded)); - assertTrue(sync.validateBlockVotes(block)); - - votes.add(key3.sign(encoded)); - assertTrue(sync.validateBlockVotes(block)); - - votes.clear(); - votes.add(new Key().sign(encoded)); - votes.add(new Key().sign(encoded)); - assertFalse(sync.validateBlockVotes(block)); - } - - @Test - public void testCheckpoints() { - Key key1 = new Key(); - List validators = Collections.singletonList(Hex.encode(key1.toAddress())); - - // mock the chain - BlockchainImpl chain = spy(new BlockchainImpl(kernelRule.getKernel().getConfig(), temporaryDBRule)); - doReturn(validators).when(chain).getValidators(); - kernelRule.getKernel().setBlockchain(chain); - - // mock sync manager - SemuxSync sync = spy(new SemuxSync(kernelRule.getKernel())); - - // prepare block - Block block = kernelRule.createBlock(Collections.emptyList()); - Vote vote = new Vote(VoteType.PRECOMMIT, Vote.VALUE_APPROVE, block.getNumber(), block.getView(), - block.getHash()); - block.setVotes(Collections.singletonList(vote.sign(key1).getSignature())); - - // mock checkpoints - Map checkpoints = new HashMap<>(); - checkpoints.put(block.getNumber(), RandomUtils.nextBytes(32)); - Config config = spy(kernelRule.getKernel().getConfig()); - when(config.checkpoints()).thenReturn(checkpoints); - TestUtils.setInternalState(sync, "config", config, SemuxSync.class); - - // tests - assertFalse(sync.validateBlock(block, chain.getAccountState(), chain.getDelegateState())); - } - @Test public void testFastSync() throws Exception { List keys = new ArrayList<>(); @@ -282,17 +114,17 @@ public void testFastSync() throws Exception { TreeSet> currentSet = TestUtils.getInternalState(sync, "currentSet", SemuxSync.class); Map> toFinalize = TestUtils.getInternalState(sync, "toFinalize", SemuxSync.class); - assert (toProcess.isEmpty()); - assert (currentSet.isEmpty()); - assert (toFinalize.size() == validatorInterval); + assertTrue(toProcess.isEmpty()); + assertTrue(currentSet.isEmpty()); + assertTrue(toFinalize.size() == validatorInterval); assertTrue(TestUtils.getInternalState(sync, "fastSync", SemuxSync.class)); for (int i = 0; i < validatorInterval; i++) { sync.process(); } - assert (toFinalize.isEmpty()); - assert (chain.getLatestBlockNumber() == validatorInterval); + assertTrue(toFinalize.isEmpty()); + assertTrue(chain.getLatestBlockNumber() == validatorInterval); assertFalse(TestUtils.getInternalState(sync, "fastSync", SemuxSync.class)); } @@ -326,7 +158,7 @@ public void testNormalSync() throws Exception { TestUtils.setInternalState(sync, "toProcess", toProcess, SemuxSync.class); AtomicLong target = TestUtils.getInternalState(sync, "target", SemuxSync.class); target.set(validatorInterval - 1); // when the remaining number of blocks to sync < validatorInterval fastSync - // is not activated + // is not activated Block block = kernelRule.createBlock(Collections.emptyList()); Vote vote = new Vote(VoteType.PRECOMMIT, Vote.VALUE_REJECT, block.getNumber(), block.getView(), @@ -343,9 +175,9 @@ public void testNormalSync() throws Exception { sync.process(); // when fastSync is not activated, votes are validated for each block - assert (toProcess.isEmpty()); + assertTrue(toProcess.isEmpty()); assertFalse(TestUtils.getInternalState(sync, "fastSync", SemuxSync.class)); - assert (chain.getLatestBlockNumber() == 0); + assertTrue(chain.getLatestBlockNumber() == 0); vote = new Vote(VoteType.PRECOMMIT, Vote.VALUE_APPROVE, block.getNumber(), block.getView(), block.getHash()); @@ -360,9 +192,9 @@ public void testNormalSync() throws Exception { toProcess.add(Pair.of(block, channel)); sync.process(); - assert (toProcess.isEmpty()); + assertTrue(toProcess.isEmpty()); assertFalse(TestUtils.getInternalState(sync, "fastSync", SemuxSync.class)); - assert (chain.getLatestBlockNumber() == 1); + assertTrue(chain.getLatestBlockNumber() == 1); target.set(10 * validatorInterval); // fastSync is activated only at the beginning of a validator set sync.process(); @@ -413,8 +245,8 @@ public void testValidateSetHashes() throws Exception { sync.validateSetHashes(); - assert (currentSet.size() == validatorInterval / 2); - assert (toFinalize.isEmpty()); + assertTrue(currentSet.size() == validatorInterval / 2); + assertTrue(toFinalize.isEmpty()); Block invalidBlock = kernelRule.createBlock(Collections.emptyList(), currentSet.last().getKey().getHeader()); Block validBlock = kernelRule.createBlock(Collections.emptyList(), currentSet.last().getKey().getHeader()); @@ -433,9 +265,9 @@ public void testValidateSetHashes() throws Exception { sync.validateSetHashes(); - assert (currentSet.size() == validatorInterval - 1); - assert (toFinalize.isEmpty()); - assert (toDownload.contains(validatorInterval)); + assertTrue(currentSet.size() == validatorInterval - 1); + assertTrue(toFinalize.isEmpty()); + assertTrue(toDownload.contains(validatorInterval)); Vote vote = new Vote(VoteType.PRECOMMIT, Vote.VALUE_APPROVE, lastBlock.getNumber(), lastBlock.getView(), lastBlock.getHash()); @@ -450,21 +282,21 @@ public void testValidateSetHashes() throws Exception { currentSet.add(Pair.of(lastBlock, channel)); sync.validateSetHashes(); - assert (currentSet.size() == validatorInterval / 2); - assert (toFinalize.size() == validatorInterval - currentSet.size() - 1); - assert (toDownload.contains(validatorInterval - toFinalize.size())); + assertTrue(currentSet.size() == validatorInterval / 2); + assertTrue(toFinalize.size() == validatorInterval - currentSet.size() - 1); + assertTrue(toDownload.contains(validatorInterval - toFinalize.size())); Channel channel2 = new Channel(null); currentSet.add(Pair.of(lastBlock, channel2)); sync.validateSetHashes(); - assert (currentSet.size() == validatorInterval / 2); - assert (toFinalize.size() == validatorInterval - currentSet.size() - 1); + assertTrue(currentSet.size() == validatorInterval / 2); + assertTrue(toFinalize.size() == validatorInterval - currentSet.size() - 1); currentSet.add(Pair.of(validBlock, channel)); sync.validateSetHashes(); - assert (currentSet.isEmpty()); - assert (toFinalize.size() == validatorInterval); + assertTrue(currentSet.isEmpty()); + assertTrue(toFinalize.size() == validatorInterval); } } diff --git a/src/test/java/org/semux/core/BlockTest.java b/src/test/java/org/semux/core/BlockTest.java index 13e0e2b23..bd6a3f09a 100644 --- a/src/test/java/org/semux/core/BlockTest.java +++ b/src/test/java/org/semux/core/BlockTest.java @@ -118,7 +118,7 @@ public void testValidateTransactions() { resultsRoot, stateRoot, data); Block block = new Block(header, transactions); - assertTrue(block.validateHeader(previousHeader, header)); + assertTrue(block.validateHeader(header, previousHeader)); assertTrue(block.validateTransactions(previousHeader, transactions, Network.DEVNET)); assertTrue(block.validateResults(previousHeader, results)); } @@ -131,7 +131,7 @@ public void testValidateTransactionsSparse() { resultsRoot, stateRoot, data); Block block = new Block(header, transactions); - assertTrue(block.validateHeader(previousHeader, header)); + assertTrue(block.validateHeader(header, previousHeader)); assertTrue(block.validateTransactions(previousHeader, Collections.singleton(transactions.get(0)), transactions, Network.DEVNET)); assertTrue(block.validateResults(previousHeader, results)); diff --git a/src/test/java/org/semux/core/BlockchainImportTest.java b/src/test/java/org/semux/core/BlockchainImportTest.java new file mode 100644 index 000000000..43ed41090 --- /dev/null +++ b/src/test/java/org/semux/core/BlockchainImportTest.java @@ -0,0 +1,188 @@ +/** + * Copyright (c) 2017-2018 The Semux Developers + * + * Distributed under the MIT software license, see the accompanying file + * LICENSE or https://opensource.org/licenses/mit-license.php + */ +package org.semux.core; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; +import static org.semux.core.Amount.Unit.SEM; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.commons.lang3.RandomUtils; +import org.junit.Rule; +import org.junit.Test; +import org.semux.TestUtils; +import org.semux.config.Config; +import org.semux.config.Constants; +import org.semux.consensus.SemuxSync; +import org.semux.consensus.Vote; +import org.semux.consensus.VoteType; +import org.semux.crypto.Hex; +import org.semux.crypto.Key; +import org.semux.rules.KernelRule; +import org.semux.rules.TemporaryDatabaseRule; +import org.semux.util.Bytes; +import org.semux.util.TimeUtil; + +public class BlockchainImportTest { + + @Rule + public KernelRule kernelRule = new KernelRule(51610, 51710); + + @Rule + public TemporaryDatabaseRule temporaryDBRule = new TemporaryDatabaseRule(); + + @Test + public void testDuplicatedTransaction() { + // mock blockchain with a single transaction + Key to = new Key(); + Key from1 = new Key(); + long time = TimeUtil.currentTimeMillis(); + Transaction tx1 = new Transaction( + kernelRule.getKernel().getConfig().network(), + TransactionType.TRANSFER, + to.toAddress(), + SEM.of(10), + kernelRule.getKernel().getConfig().spec().minTransactionFee(), + 0, + time, + Bytes.EMPTY_BYTES).sign(from1); + kernelRule.getKernel().setBlockchain(new BlockchainImpl(kernelRule.getKernel().getConfig(), temporaryDBRule)); + kernelRule.getKernel().getBlockchain().getAccountState().adjustAvailable(from1.toAddress(), SEM.of(1000)); + Block block1 = kernelRule.createBlock(Collections.singletonList(tx1)); + kernelRule.getKernel().getBlockchain().addBlock(block1); + SemuxSync semuxSync = spy(new SemuxSync(kernelRule.getKernel())); + + // create a tx with the same hash with tx1 from a different signer in the second + // block + Key from2 = new Key(); + kernelRule.getKernel().getBlockchain().getAccountState().adjustAvailable(from2.toAddress(), SEM.of(1000)); + Transaction tx2 = new Transaction( + kernelRule.getKernel().getConfig().network(), + TransactionType.TRANSFER, + to.toAddress(), + SEM.of(10), + kernelRule.getKernel().getConfig().spec().minTransactionFee(), + 0, + time, + Bytes.EMPTY_BYTES).sign(from2); + Block block2 = kernelRule.createBlock(Collections.singletonList(tx2)); + + // this test case is valid if and only if tx1 and tx2 have the same tx hash + assert (Arrays.equals(tx1.getHash(), tx2.getHash())); + + // the block should be rejected because of the duplicated tx + assertFalse(kernelRule.getKernel().getBlockchain().importBlock(block2, false)); + } + + @Test + public void testValidateCoinbaseMagic() { + BlockchainImpl blockchain = spy(new BlockchainImpl(kernelRule.getKernel().getConfig(), temporaryDBRule)); + kernelRule.enableForks(Fork.UNIFORM_DISTRIBUTION); + kernelRule.getKernel().setBlockchain(blockchain); + + // block.coinbase = coinbase magic account + Block block = TestUtils.createBlock( + kernelRule.getKernel().getBlockchain().getLatestBlock().getHash(), + Constants.COINBASE_KEY, + kernelRule.getKernel().getBlockchain().getLatestBlockNumber() + 1, + Collections.emptyList(), + Collections.emptyList()); + + assertFalse(kernelRule.getKernel().getBlockchain().importBlock(block, true)); + + // tx.to = coinbase magic account + Transaction tx = TestUtils.createTransaction(kernelRule.getKernel().getConfig(), new Key(), + Constants.COINBASE_KEY, Amount.ZERO); + Block block2 = TestUtils.createBlock( + kernelRule.getKernel().getBlockchain().getLatestBlock().getHash(), + new Key(), + kernelRule.getKernel().getBlockchain().getLatestBlockNumber() + 1, + Collections.singletonList(tx), + Collections.singletonList(new TransactionResult())); + + assertFalse(kernelRule.getKernel().getBlockchain().importBlock(block2, true)); + } + + @Test + public void testValidateBlockVotes() { + Key key1 = new Key(); + Key key2 = new Key(); + Key key3 = new Key(); + List validators = Arrays.asList(Hex.encode(key1.toAddress()), + Hex.encode(key2.toAddress()), + Hex.encode(key3.toAddress())); + + // mock the chain + BlockchainImpl chain = spy(new BlockchainImpl(kernelRule.getKernel().getConfig(), temporaryDBRule)); + doReturn(validators).when(chain).getValidators(); + kernelRule.getKernel().setBlockchain(chain); + + // mock sync manager + SemuxSync sync = spy(new SemuxSync(kernelRule.getKernel())); + + // prepare block + Block block = kernelRule.createBlock(Collections.emptyList()); + Vote vote = new Vote(VoteType.PRECOMMIT, Vote.VALUE_APPROVE, block.getNumber(), block.getView(), + block.getHash()); + byte[] encoded = vote.getEncoded(); + List votes = new ArrayList<>(); + block.setVotes(votes); + + // tests + assertFalse(chain.validateBlockVotes(block)); + + votes.add(key1.sign(encoded)); + votes.add(key1.sign(encoded)); + assertFalse(chain.validateBlockVotes(block)); + + votes.add(key2.sign(encoded)); + assertTrue(chain.validateBlockVotes(block)); + + votes.add(key3.sign(encoded)); + assertTrue(chain.validateBlockVotes(block)); + + votes.clear(); + votes.add(new Key().sign(encoded)); + votes.add(new Key().sign(encoded)); + assertFalse(chain.validateBlockVotes(block)); + } + + @Test + public void testCheckpoints() { + Key key1 = new Key(); + List validators = Collections.singletonList(Hex.encode(key1.toAddress())); + + // mock checkpoints + Map checkpoints = new HashMap<>(); + checkpoints.put(1L, RandomUtils.nextBytes(32)); + Config config = spy(kernelRule.getKernel().getConfig()); + when(config.checkpoints()).thenReturn(checkpoints); + + // mock the chain + BlockchainImpl chain = spy(new BlockchainImpl(config, temporaryDBRule)); + doReturn(validators).when(chain).getValidators(); + kernelRule.getKernel().setBlockchain(chain); + + // prepare block + Block block = kernelRule.createBlock(Collections.emptyList()); + Vote vote = new Vote(VoteType.PRECOMMIT, Vote.VALUE_APPROVE, block.getNumber(), block.getView(), + block.getHash()); + block.setVotes(Collections.singletonList(vote.sign(key1).getSignature())); + + // tests + assertFalse(chain.importBlock(block, false)); + } +} diff --git a/src/test/java/org/semux/core/CorePerformanceTest.java b/src/test/java/org/semux/core/CorePerformanceTest.java index 9d4ace243..c2acca65f 100644 --- a/src/test/java/org/semux/core/CorePerformanceTest.java +++ b/src/test/java/org/semux/core/CorePerformanceTest.java @@ -88,7 +88,7 @@ public void testTransactionProcessing() { t1 = System.nanoTime(); exec.execute(txs, chain.getAccountState().track(), chain.getDelegateState().track(), - new SemuxBlock(chain.getLatestBlock().getHeader(), config.spec().maxBlockGasLimit()), null); + new SemuxBlock(chain.getLatestBlock().getHeader(), config.spec().maxBlockGasLimit()), null, 0); t2 = System.nanoTime(); logger.info("Perf_transaction_2: {} μs/tx", (t2 - t1) / 1_000 / repeat); } diff --git a/src/test/java/org/semux/core/TransactionExecutorTest.java b/src/test/java/org/semux/core/TransactionExecutorTest.java index 64e066525..407ca0ba6 100644 --- a/src/test/java/org/semux/core/TransactionExecutorTest.java +++ b/src/test/java/org/semux/core/TransactionExecutorTest.java @@ -61,7 +61,7 @@ public void prepare() { private TransactionResult executeAndCommit(TransactionExecutor exec, Transaction tx, AccountState as, DelegateState ds, SemuxBlock bh) { - TransactionResult res = exec.execute(tx, as, ds, bh, chain); + TransactionResult res = exec.execute(tx, as, ds, bh, chain, 0); as.commit(); ds.commit(); @@ -86,14 +86,14 @@ public void testTransfer() { assertTrue(tx.validate(network)); // insufficient available - TransactionResult result = exec.execute(tx, as.track(), ds.track(), block, chain); + TransactionResult result = exec.execute(tx, as.track(), ds.track(), block, chain, 0); assertFalse(result.getCode().isSuccess()); Amount available = SEM.of(1000); as.adjustAvailable(key.toAddress(), available); // execute but not commit - result = exec.execute(tx, as.track(), ds.track(), block, chain); + result = exec.execute(tx, as.track(), ds.track(), block, chain, 0); assertTrue(result.getCode().isSuccess()); assertEquals(available, as.getAccount(key.toAddress()).getAvailable()); assertEquals(ZERO, as.getAccount(to).getAvailable()); @@ -123,12 +123,12 @@ public void testDelegate() { // register delegate (to != EMPTY_ADDRESS, random name) Transaction tx = new Transaction(network, type, to, value, fee, nonce, timestamp, data).sign(delegate); - TransactionResult result = exec.execute(tx, as.track(), ds.track(), block, chain); + TransactionResult result = exec.execute(tx, as.track(), ds.track(), block, chain, 0); assertFalse(result.getCode().isSuccess()); // register delegate (to == EMPTY_ADDRESS, random name) tx = new Transaction(network, type, Bytes.EMPTY_ADDRESS, value, fee, nonce, timestamp, data).sign(delegate); - result = exec.execute(tx, as.track(), ds.track(), block, chain); + result = exec.execute(tx, as.track(), ds.track(), block, chain, 0); assertFalse(result.getCode().isSuccess()); // register delegate (to == EMPTY_ADDRESS, normal name) and commit @@ -162,7 +162,7 @@ public void testVote() { // vote for non-existing delegate Transaction tx = new Transaction(network, type, to, value, fee, nonce, timestamp, data).sign(voter); - TransactionResult result = exec.execute(tx, as.track(), ds.track(), block, chain); + TransactionResult result = exec.execute(tx, as.track(), ds.track(), block, chain, 0); assertFalse(result.getCode().isSuccess()); ds.register(delegate.toAddress(), Bytes.of("delegate")); @@ -196,13 +196,13 @@ public void testUnvote() { // unvote (never voted before) Transaction tx = new Transaction(network, type, to, value, fee, nonce, timestamp, data).sign(voter); - TransactionResult result = exec.execute(tx, as.track(), ds.track(), block, chain); + TransactionResult result = exec.execute(tx, as.track(), ds.track(), block, chain, 0); assertFalse(result.getCode().isSuccess()); assertEquals(INSUFFICIENT_LOCKED, result.code); ds.vote(voter.toAddress(), delegate.toAddress(), value); // unvote (locked = 0) - result = exec.execute(tx, as.track(), ds.track(), block, chain); + result = exec.execute(tx, as.track(), ds.track(), block, chain, 0); assertFalse(result.getCode().isSuccess()); assertEquals(INSUFFICIENT_LOCKED, result.code); @@ -236,7 +236,7 @@ public void testUnvoteInsufficientFee() { // unvote (never voted before) Transaction tx = new Transaction(network, type, to, value, fee, nonce, timestamp, data).sign(voter); - TransactionResult result = exec.execute(tx, as.track(), ds.track(), block, chain); + TransactionResult result = exec.execute(tx, as.track(), ds.track(), block, chain, 0); assertFalse(result.getCode().isSuccess()); assertEquals(INSUFFICIENT_AVAILABLE, result.code); } diff --git a/src/test/java/org/semux/integration/SyncingTest.java b/src/test/java/org/semux/integration/SyncingTest.java index c472a4ece..15150f8fb 100644 --- a/src/test/java/org/semux/integration/SyncingTest.java +++ b/src/test/java/org/semux/integration/SyncingTest.java @@ -140,7 +140,7 @@ public void testSync() throws IOException { for (int i = 1; i <= targetHeight(); i++) { Block block = kernel4.getBlockchain().getBlock(i); Block previousBlock = kernel4.getBlockchain().getBlock(i - 1); - assertTrue(block.validateHeader(previousBlock.getHeader(), block.getHeader())); + assertTrue(block.validateHeader(block.getHeader(), previousBlock.getHeader())); assertTrue(block.validateTransactions(previousBlock.getHeader(), block.getTransactions(), kernel4.getConfig().network())); assertTrue(block.validateResults(previousBlock.getHeader(), block.getResults())); diff --git a/src/test/java/org/semux/vm/client/InternalTransactionTest.java b/src/test/java/org/semux/vm/client/InternalTransactionTest.java index 4748cc040..9196cbd94 100644 --- a/src/test/java/org/semux/vm/client/InternalTransactionTest.java +++ b/src/test/java/org/semux/vm/client/InternalTransactionTest.java @@ -100,7 +100,7 @@ public void testInternalTransfer() { Transaction tx = new Transaction(network, type, to, value, Amount.ZERO, nonce, timestamp, data, gas, gasPrice); tx.sign(key); - TransactionResult result = exec.execute(tx, as, ds, bh, chain); + TransactionResult result = exec.execute(tx, as, ds, bh, chain, 0); assertTrue(result.getCode().isSuccess()); assertEquals(SEM.of(1000).getNano() - SEM.of(5).getNano(), as.getAccount(to).getAvailable().getNano()); assertEquals(SEM.of(5).getNano(), diff --git a/src/test/java/org/semux/vm/client/PrecompiledContractTest.java b/src/test/java/org/semux/vm/client/PrecompiledContractTest.java index 161214d50..2e13d72ee 100644 --- a/src/test/java/org/semux/vm/client/PrecompiledContractTest.java +++ b/src/test/java/org/semux/vm/client/PrecompiledContractTest.java @@ -89,7 +89,7 @@ public void testSuccess() { .decode("60806040526004361061004c576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806302aa9be2146100515780635f74bbde1461009e575b600080fd5b34801561005d57600080fd5b5061009c600480360381019080803573ffffffffffffffffffffffffffffffffffffffff169060200190929190803590602001909291905050506100eb565b005b3480156100aa57600080fd5b506100e9600480360381019080803573ffffffffffffffffffffffffffffffffffffffff16906020019092919080359060200190929190505050610165565b005b606573ffffffffffffffffffffffffffffffffffffffff168282604051808373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001828152602001925050506000604051808303816000865af1915050151561016157600080fd5b5050565b606473ffffffffffffffffffffffffffffffffffffffff168282604051808373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001828152602001925050506000604051808303816000865af191505015156101db57600080fd5b50505600a165627a7a723058205e1325476a012c885717ccd0ad038a0d8d6c20f2e5afcb1b475d3eac5313c9500029"); as.setCode(to, contract); - SemuxBlock bh = new SemuxBlock( + SemuxBlock block = new SemuxBlock( new BlockHeader(123l, Bytes.random(20), Bytes.random(20), System.currentTimeMillis(), Bytes.random(20), Bytes.random(20), Bytes.random(20), Bytes.random(20)), config.spec().maxBlockGasLimit()); @@ -111,7 +111,7 @@ public void testSuccess() { dataGasCost += (b == 0 ? 4 : 68); } - TransactionResult result = exec.execute(tx, as, ds, bh, chain); + TransactionResult result = exec.execute(tx, as, ds, block, chain, 0); assertTrue(result.getCode().isSuccess()); assertEquals(21_000 + 21_000 + dataGasCost + 1088, result.getGasUsed()); assertEquals(SEM.of(1000).getNano() - result.getGasUsed() * gasPrice.getNano(), @@ -128,7 +128,7 @@ public void testSuccess() { tx = new Transaction(network, type, to, value, Amount.ZERO, nonce + 1, timestamp, data, gas, gasPrice); tx.sign(key); - result = exec.execute(tx, as, ds, bh, chain); + result = exec.execute(tx, as, ds, block, chain, 0); assertTrue(result.getCode().isSuccess()); assertEquals(SEM.of(1000).getNano(), as.getAccount(to).getAvailable().getNano()); assertEquals(0, as.getAccount(to).getLocked().getNano()); diff --git a/src/test/java/org/semux/vm/client/VmTransactionTest.java b/src/test/java/org/semux/vm/client/VmTransactionTest.java index 4347984d5..c6fe216de 100644 --- a/src/test/java/org/semux/vm/client/VmTransactionTest.java +++ b/src/test/java/org/semux/vm/client/VmTransactionTest.java @@ -109,20 +109,20 @@ public void testCall() { assertTrue(tx.validate(network)); // insufficient available - TransactionResult result = exec.execute(tx, as.track(), ds.track(), block, chain); + TransactionResult result = exec.execute(tx, as.track(), ds.track(), block, chain, 0); assertFalse(result.getCode().isSuccess()); Amount available = SEM.of(1000); as.adjustAvailable(key.toAddress(), available); // execute but not commit - result = exec.execute(tx, as.track(), ds.track(), block, chain); + result = exec.execute(tx, as.track(), ds.track(), block, chain, 0); assertTrue(result.getCode().isSuccess()); assertEquals(available, as.getAccount(key.toAddress()).getAvailable()); assertEquals(ZERO, as.getAccount(to).getAvailable()); // execute and commit - result = exec.execute(tx, as, ds, block, chain); + result = exec.execute(tx, as, ds, block, chain, 0); assertTrue(result.getCode().isSuccess()); // miner're reward is not yet given @@ -157,20 +157,20 @@ public void testCreate() { assertTrue(tx.validate(network)); // insufficient available - TransactionResult result = exec.execute(tx, as.track(), ds.track(), block, chain); + TransactionResult result = exec.execute(tx, as.track(), ds.track(), block, chain, 0); assertFalse(result.getCode().isSuccess()); Amount available = SEM.of(1000); as.adjustAvailable(key.toAddress(), available); // execute but not commit - result = exec.execute(tx, as.track(), ds.track(), block, chain); + result = exec.execute(tx, as.track(), ds.track(), block, chain, 0); assertTrue(result.getCode().isSuccess()); assertEquals(available, as.getAccount(key.toAddress()).getAvailable()); assertEquals(ZERO, as.getAccount(to).getAvailable()); // execute and commit - result = exec.execute(tx, as, ds, block, chain); + result = exec.execute(tx, as, ds, block, chain, 0); assertTrue(result.getCode().isSuccess()); byte[] newContractAddress = HashUtil.calcNewAddress(tx.getFrom(), tx.getNonce()); @@ -204,7 +204,7 @@ public void testTransferToContract() { Transaction tx = new Transaction(network, type, to, value, Amount.ZERO, nonce, timestamp, data, gas, gasPrice); tx.sign(key); - TransactionResult result = exec.execute(tx, as, ds, block, chain); + TransactionResult result = exec.execute(tx, as, ds, block, chain, 0); assertTrue(result.getCode().isSuccess()); assertEquals(SEM.of(1000).getNano() - value.getNano() @@ -238,7 +238,7 @@ public void testTransferToContractOutOfGas() { Transaction tx = new Transaction(network, type, to, value, Amount.ZERO, nonce, timestamp, data, gas, gasPrice); tx.sign(key); - TransactionResult result = exec.execute(tx, as, ds, block, chain); + TransactionResult result = exec.execute(tx, as, ds, block, chain, 0); assertTrue(result.getCode().isFailure()); // value transfer reverted assertEquals(SEM.of(1000).getNano() - tx.getGasPrice().getNano() * result.getGasUsed(),