mirror of
https://github.com/Qortal/altcoinj.git
synced 2025-02-12 02:05:53 +00:00
Transaction.verify: make it throw VerificationException subclasses, and add a check for duplicated outpoints.
This check does not have any impact on existing apps as in full verification mode the UTXO set is already effectively performing this check, and general SPV wallets don't verify inputs meaningfully anyway (they could just be random). However it's useful for Lighthouse and can't hurt - a tx with a duplicated outpoint is always invalid.
This commit is contained in:
parent
4d18a477dd
commit
ed75774605
@ -1163,38 +1163,57 @@ public class Transaction extends ChildMessage implements Serializable {
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the transaction contents for sanity, in ways that can be done in a standalone manner.
|
||||
* <p>Checks the transaction contents for sanity, in ways that can be done in a standalone manner.
|
||||
* Does <b>not</b> perform all checks on a transaction such as whether the inputs are already spent.
|
||||
* Specifically this method verifies:</p>
|
||||
*
|
||||
* <ul>
|
||||
* <li>That there is at least one input and output.</li>
|
||||
* <li>That the serialized size is not larger than the max block size.</li>
|
||||
* <li>That no outputs have negative value.</li>
|
||||
* <li>That the outputs do not sum to larger than the max allowed quantity of coin in the system.</li>
|
||||
* <li>If the tx is a coinbase tx, the coinbase scriptSig size is within range. Otherwise that there are no
|
||||
* coinbase inputs in the tx.</li>
|
||||
* </ul>
|
||||
*
|
||||
* @throws VerificationException
|
||||
*/
|
||||
public void verify() throws VerificationException {
|
||||
maybeParse();
|
||||
if (inputs.size() == 0 || outputs.size() == 0)
|
||||
throw new VerificationException("Transaction had no inputs or no outputs.");
|
||||
throw new VerificationException.EmptyInputsOrOutputs();
|
||||
if (this.getMessageSize() > Block.MAX_BLOCK_SIZE)
|
||||
throw new VerificationException("Transaction larger than MAX_BLOCK_SIZE");
|
||||
throw new VerificationException.LargerThanMaxBlockSize();
|
||||
|
||||
Coin valueOut = Coin.ZERO;
|
||||
HashSet<TransactionOutPoint> outpoints = new HashSet<TransactionOutPoint>();
|
||||
for (TransactionInput input : inputs) {
|
||||
if (outpoints.contains(input.getOutpoint()))
|
||||
throw new VerificationException.DuplicatedOutPoint();
|
||||
outpoints.add(input.getOutpoint());
|
||||
}
|
||||
try {
|
||||
for (TransactionOutput output : outputs) {
|
||||
if (output.getValue().signum() < 0)
|
||||
throw new VerificationException("Transaction output negative");
|
||||
if (output.getValue().signum() < 0) // getValue() can throw IllegalStateException
|
||||
throw new VerificationException.NegativeValueOutput();
|
||||
valueOut = valueOut.add(output.getValue());
|
||||
// Duplicate the MAX_MONEY check from Coin.add() in case someone accidentally removes it.
|
||||
if (valueOut.compareTo(NetworkParameters.MAX_MONEY) > 0)
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
} catch (IllegalStateException e) {
|
||||
throw new VerificationException("A transaction output value exceeds maximum possible");
|
||||
throw new VerificationException.ExcessiveValue();
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new VerificationException("Total transaction output value greater than possible");
|
||||
throw new VerificationException.ExcessiveValue();
|
||||
}
|
||||
|
||||
if (isCoinBase()) {
|
||||
if (inputs.get(0).getScriptBytes().length < 2 || inputs.get(0).getScriptBytes().length > 100)
|
||||
throw new VerificationException("Coinbase script size out of range");
|
||||
throw new VerificationException.CoinbaseScriptSizeOutOfRange();
|
||||
} else {
|
||||
for (TransactionInput input : inputs)
|
||||
if (input.isCoinBase())
|
||||
throw new VerificationException("Coinbase input as input in non-coinbase transaction");
|
||||
throw new VerificationException.UnexpectedCoinbaseInput();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -29,4 +29,48 @@ public class VerificationException extends RuntimeException {
|
||||
public VerificationException(String msg, Throwable t) {
|
||||
super(msg, t);
|
||||
}
|
||||
|
||||
public static class EmptyInputsOrOutputs extends VerificationException {
|
||||
public EmptyInputsOrOutputs() {
|
||||
super("Transaction had no inputs or no outputs.");
|
||||
}
|
||||
}
|
||||
|
||||
public static class LargerThanMaxBlockSize extends VerificationException {
|
||||
public LargerThanMaxBlockSize() {
|
||||
super("Transaction larger than MAX_BLOCK_SIZE");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static class DuplicatedOutPoint extends VerificationException {
|
||||
public DuplicatedOutPoint() {
|
||||
super("Duplicated outpoint");
|
||||
}
|
||||
}
|
||||
|
||||
public static class NegativeValueOutput extends VerificationException {
|
||||
public NegativeValueOutput() {
|
||||
super("Transaction output negative");
|
||||
}
|
||||
}
|
||||
|
||||
public static class ExcessiveValue extends VerificationException {
|
||||
public ExcessiveValue() {
|
||||
super("Total transaction output value greater than possible");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static class CoinbaseScriptSizeOutOfRange extends VerificationException {
|
||||
public CoinbaseScriptSizeOutOfRange() {
|
||||
super("Coinbase script size out of range");
|
||||
}
|
||||
}
|
||||
|
||||
public static class UnexpectedCoinbaseInput extends VerificationException {
|
||||
public UnexpectedCoinbaseInput() {
|
||||
super("Coinbase input as input in non-coinbase transaction");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,92 @@
|
||||
package com.google.bitcoin.core;
|
||||
|
||||
import com.google.bitcoin.params.UnitTestParams;
|
||||
import com.google.bitcoin.script.ScriptBuilder;
|
||||
import com.google.bitcoin.testing.FakeTxBuilder;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
/**
|
||||
* Just check the Transaction.verify() method. Most methods that have complicated logic in Transaction are tested
|
||||
* elsewhere, e.g. signing and hashing are well exercised by the wallet tests, the full block chain tests and so on.
|
||||
* The verify method is also exercised by the full block chain tests, but it can also be used by API users alone,
|
||||
* so we make sure to cover it here as well.
|
||||
*/
|
||||
public class TransactionTest {
|
||||
private static final NetworkParameters PARAMS = UnitTestParams.get();
|
||||
private Transaction tx;
|
||||
private Transaction dummy;
|
||||
|
||||
public static final Address ADDRESS = new ECKey().toAddress(PARAMS);
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
dummy = FakeTxBuilder.createFakeTx(PARAMS, Coin.COIN, ADDRESS);
|
||||
tx = new Transaction(PARAMS);
|
||||
tx.addOutput(Coin.COIN, ADDRESS);
|
||||
tx.addInput(dummy.getOutput(0));
|
||||
}
|
||||
|
||||
@Test(expected = VerificationException.EmptyInputsOrOutputs.class)
|
||||
public void emptyOutputs() throws Exception {
|
||||
tx.clearOutputs();
|
||||
tx.verify();
|
||||
}
|
||||
|
||||
@Test(expected = VerificationException.EmptyInputsOrOutputs.class)
|
||||
public void emptyInputs() throws Exception {
|
||||
tx.clearInputs();
|
||||
tx.verify();
|
||||
}
|
||||
|
||||
@Test(expected = VerificationException.LargerThanMaxBlockSize.class)
|
||||
public void tooHuge() throws Exception {
|
||||
tx.addInput(dummy.getOutput(0)).setScriptBytes(new byte[Block.MAX_BLOCK_SIZE]);
|
||||
tx.verify();
|
||||
}
|
||||
|
||||
@Test(expected = VerificationException.DuplicatedOutPoint.class)
|
||||
public void duplicateOutPoint() throws Exception {
|
||||
TransactionInput input = tx.getInput(0);
|
||||
input.setScriptBytes(new byte[1]);
|
||||
tx.addInput(input.duplicateDetached());
|
||||
tx.verify();
|
||||
}
|
||||
|
||||
@Test(expected = VerificationException.NegativeValueOutput.class)
|
||||
public void negativeOutput() throws Exception {
|
||||
tx.getOutput(0).setValue(Coin.NEGATIVE_SATOSHI);
|
||||
tx.verify();
|
||||
}
|
||||
|
||||
@Test(expected = VerificationException.ExcessiveValue.class)
|
||||
public void exceedsMaxMoney2() throws Exception {
|
||||
Coin half = NetworkParameters.MAX_MONEY.divide(2).add(Coin.SATOSHI);
|
||||
tx.getOutput(0).setValue(half);
|
||||
tx.addOutput(half, ADDRESS);
|
||||
tx.verify();
|
||||
}
|
||||
|
||||
@Test(expected = VerificationException.UnexpectedCoinbaseInput.class)
|
||||
public void coinbaseInputInNonCoinbaseTX() throws Exception {
|
||||
tx.addInput(Sha256Hash.ZERO_HASH, 0xFFFFFFFFL, new ScriptBuilder().data(new byte[10]).build());
|
||||
tx.verify();
|
||||
}
|
||||
|
||||
@Test(expected = VerificationException.CoinbaseScriptSizeOutOfRange.class)
|
||||
public void coinbaseScriptSigTooSmall() throws Exception {
|
||||
tx.clearInputs();
|
||||
tx.addInput(Sha256Hash.ZERO_HASH, 0xFFFFFFFFL, new ScriptBuilder().build());
|
||||
tx.verify();
|
||||
}
|
||||
|
||||
@Test(expected = VerificationException.CoinbaseScriptSizeOutOfRange.class)
|
||||
public void coinbaseScriptSigTooLarge() throws Exception {
|
||||
tx.clearInputs();
|
||||
TransactionInput input = tx.addInput(Sha256Hash.ZERO_HASH, 0xFFFFFFFFL, new ScriptBuilder().data(new byte[99]).build());
|
||||
assertEquals(101, input.getScriptBytes().length);
|
||||
tx.verify();
|
||||
}
|
||||
}
|
@ -2260,14 +2260,15 @@ public class WalletTest extends TestWithWallet {
|
||||
Coin outputValue = Transaction.MIN_NONDUST_OUTPUT.add(Transaction.REFERENCE_DEFAULT_MIN_TX_FEE).subtract(SATOSHI);
|
||||
tx = createFakeTx(params, outputValue, myAddress);
|
||||
wallet.receiveFromBlock(tx, block3, AbstractBlockChain.NewBlockType.BEST_CHAIN, 0);
|
||||
request = SendRequest.emptyWallet(outputKey);
|
||||
try {
|
||||
request = SendRequest.emptyWallet(outputKey);
|
||||
wallet.completeTx(request);
|
||||
fail();
|
||||
} catch (Wallet.CouldNotAdjustDownwards e) {}
|
||||
request = SendRequest.emptyWallet(outputKey);
|
||||
request.ensureMinRequiredFee = false;
|
||||
wallet.completeTx(request);
|
||||
assertEquals(outputValue, request.tx.getFee());
|
||||
assertEquals(ZERO, request.tx.getFee());
|
||||
wallet.commitTx(request.tx);
|
||||
assertEquals(ZERO, wallet.getBalance());
|
||||
assertEquals(outputValue, request.tx.getOutput(0).getValue());
|
||||
|
Loading…
x
Reference in New Issue
Block a user