Transaction: make verify() a static helper method

This commit is contained in:
Andreas Schildbach 2023-04-01 18:02:22 +02:00
parent 3ad4879e40
commit cf8715eb4b
7 changed files with 71 additions and 70 deletions

View file

@ -684,8 +684,8 @@ public class Block extends Message {
checkTransactions(height, flags);
checkMerkleRoot();
checkSigOps();
for (Transaction transaction : transactions)
transaction.verify();
for (Transaction tx : transactions)
Transaction.verify(params, tx);
}
/**

View file

@ -713,7 +713,7 @@ public class Peer extends PeerSocketHandler {
protected void processTransaction(final Transaction tx) throws VerificationException {
// Check a few basic syntax issues to ensure the received TX isn't nonsense.
tx.verify();
tx.verify(params, tx);
lock.lock();
try {
if (log.isDebugEnabled())

View file

@ -1727,59 +1727,6 @@ public class Transaction extends Message {
.orElse(null);
}
/**
* <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 {
if (inputs.size() == 0 || outputs.size() == 0)
throw new VerificationException.EmptyInputsOrOutputs();
if (this.getMessageSize() > Block.MAX_BLOCK_SIZE)
throw new VerificationException.LargerThanMaxBlockSize();
HashSet<TransactionOutPoint> outpoints = new HashSet<>();
for (TransactionInput input : inputs) {
if (outpoints.contains(input.getOutpoint()))
throw new VerificationException.DuplicatedOutPoint();
outpoints.add(input.getOutpoint());
}
Coin valueOut = Coin.ZERO;
for (TransactionOutput output : outputs) {
Coin value = output.getValue();
if (value.signum() < 0)
throw new VerificationException.NegativeValueOutput();
try {
valueOut = valueOut.add(value);
} catch (ArithmeticException e) {
throw new VerificationException.ExcessiveValue();
}
if (params.network().exceedsMaxMoney(valueOut))
throw new VerificationException.ExcessiveValue();
}
if (isCoinBase()) {
if (inputs.get(0).getScriptBytes().length < 2 || inputs.get(0).getScriptBytes().length > 100)
throw new VerificationException.CoinbaseScriptSizeOutOfRange();
} else {
for (TransactionInput input : inputs)
if (input.isCoinBase())
throw new VerificationException.UnexpectedCoinbaseInput();
}
}
/**
* <p>A transaction is time-locked if at least one of its inputs is non-final and it has a lock time. A transaction can
* also have a relative lock time which this method doesn't tell. Use {@link #hasRelativeLockTime()} to find out.</p>
@ -1899,4 +1846,59 @@ public class Transaction extends Message {
public void setMemo(String memo) {
this.memo = memo;
}
/**
* <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>
*
* @param params parameters for the verification rules
* @param tx transaction to verify
* @throws VerificationException if at least one of the rules is violated
*/
public static void verify(NetworkParameters params, Transaction tx) throws VerificationException {
if (tx.inputs.size() == 0 || tx.outputs.size() == 0)
throw new VerificationException.EmptyInputsOrOutputs();
if (tx.getMessageSize() > Block.MAX_BLOCK_SIZE)
throw new VerificationException.LargerThanMaxBlockSize();
HashSet<TransactionOutPoint> outpoints = new HashSet<>();
for (TransactionInput input : tx.inputs) {
if (outpoints.contains(input.getOutpoint()))
throw new VerificationException.DuplicatedOutPoint();
outpoints.add(input.getOutpoint());
}
Coin valueOut = Coin.ZERO;
for (TransactionOutput output : tx.outputs) {
Coin value = output.getValue();
if (value.signum() < 0)
throw new VerificationException.NegativeValueOutput();
try {
valueOut = valueOut.add(value);
} catch (ArithmeticException e) {
throw new VerificationException.ExcessiveValue();
}
if (params.network().exceedsMaxMoney(valueOut))
throw new VerificationException.ExcessiveValue();
}
if (tx.isCoinBase()) {
if (tx.inputs.get(0).getScriptBytes().length < 2 || tx.inputs.get(0).getScriptBytes().length > 100)
throw new VerificationException.CoinbaseScriptSizeOutOfRange();
} else {
for (TransactionInput input : tx.inputs)
if (input.isCoinBase())
throw new VerificationException.UnexpectedCoinbaseInput();
}
}
}

View file

@ -345,7 +345,6 @@ public class PaymentProtocol {
@Nullable List<Protos.Output> refundOutputs, @Nullable String memo, @Nullable byte[] merchantData) {
Protos.Payment.Builder builder = Protos.Payment.newBuilder();
for (Transaction transaction : transactions) {
transaction.verify();
builder.addTransactions(ByteString.copyFrom(transaction.bitcoinSerialize()));
}
if (refundOutputs != null) {

View file

@ -2002,7 +2002,7 @@ public class Wallet extends BaseTaggableObject
// spend against one of our other pending transactions.
lock.lock();
try {
tx.verify();
Transaction.verify(params, tx);
// Ignore it if we already know about this transaction. Receiving a pending transaction never moves it
// between pools.
EnumSet<Pool> containingPools = getContainingPools(tx);
@ -2782,7 +2782,7 @@ public class Wallet extends BaseTaggableObject
* @throws VerificationException If transaction fails to verify
*/
public boolean maybeCommitTx(Transaction tx) throws VerificationException {
tx.verify();
Transaction.verify(params, tx);
lock.lock();
try {
if (pending.containsKey(tx.getTxId()))

View file

@ -87,21 +87,21 @@ public class TransactionTest {
public void emptyOutputs() {
Transaction tx = FakeTxBuilder.createFakeTx(TESTNET);
tx.clearOutputs();
tx.verify();
Transaction.verify(TESTNET, tx);
}
@Test(expected = VerificationException.EmptyInputsOrOutputs.class)
public void emptyInputs() {
Transaction tx = FakeTxBuilder.createFakeTx(TESTNET);
tx.clearInputs();
tx.verify();
Transaction.verify(TESTNET, tx);
}
@Test(expected = VerificationException.LargerThanMaxBlockSize.class)
public void tooHuge() {
Transaction tx = FakeTxBuilder.createFakeTx(TESTNET);
tx.getInput(0).setScriptBytes(new byte[Block.MAX_BLOCK_SIZE]);
tx.verify();
Transaction.verify(TESTNET, tx);
}
@Test(expected = VerificationException.DuplicatedOutPoint.class)
@ -110,14 +110,14 @@ public class TransactionTest {
TransactionInput input = tx.getInput(0);
input.setScriptBytes(new byte[1]);
tx.addInput(input);
tx.verify();
Transaction.verify(TESTNET, tx);
}
@Test(expected = VerificationException.NegativeValueOutput.class)
public void negativeOutput() {
Transaction tx = FakeTxBuilder.createFakeTx(TESTNET);
tx.getOutput(0).setValue(Coin.NEGATIVE_SATOSHI);
tx.verify();
Transaction.verify(TESTNET, tx);
}
@Test(expected = VerificationException.ExcessiveValue.class)
@ -126,14 +126,14 @@ public class TransactionTest {
Coin half = BitcoinNetwork.MAX_MONEY.divide(2).add(Coin.SATOSHI);
tx.getOutput(0).setValue(half);
tx.addOutput(half, ADDRESS);
tx.verify();
Transaction.verify(TESTNET, tx);
}
@Test(expected = VerificationException.UnexpectedCoinbaseInput.class)
public void coinbaseInputInNonCoinbaseTX() {
Transaction tx = FakeTxBuilder.createFakeTx(TESTNET);
tx.addInput(Sha256Hash.ZERO_HASH, 0xFFFFFFFFL, new ScriptBuilder().data(new byte[10]).build());
tx.verify();
Transaction.verify(TESTNET, tx);
}
@Test(expected = VerificationException.CoinbaseScriptSizeOutOfRange.class)
@ -141,7 +141,7 @@ public class TransactionTest {
Transaction tx = FakeTxBuilder.createFakeTx(TESTNET);
tx.clearInputs();
tx.addInput(Sha256Hash.ZERO_HASH, 0xFFFFFFFFL, new ScriptBuilder().build());
tx.verify();
Transaction.verify(TESTNET, tx);
}
@Test(expected = VerificationException.CoinbaseScriptSizeOutOfRange.class)
@ -150,7 +150,7 @@ public class TransactionTest {
tx.clearInputs();
TransactionInput input = tx.addInput(Sha256Hash.ZERO_HASH, 0xFFFFFFFFL, new ScriptBuilder().data(new byte[99]).build());
assertEquals(101, input.getScriptBytes().length);
tx.verify();
Transaction.verify(TESTNET, tx);
}
@Test

View file

@ -392,7 +392,7 @@ public class ScriptTest {
try {
Map<TransactionOutPoint, Script> scriptPubKeys = parseScriptPubKeys(test.get(0));
transaction = TESTNET.getDefaultSerializer().makeTransaction(ByteBuffer.wrap(ByteUtils.parseHex(test.get(1).asText().toLowerCase())));
transaction.verify();
Transaction.verify(TESTNET, transaction);
Set<VerifyFlag> verifyFlags = parseVerifyFlags(test.get(2).asText());
for (int i = 0; i < transaction.getInputs().size(); i++) {
@ -433,7 +433,7 @@ public class ScriptTest {
boolean valid = true;
try {
transaction.verify();
Transaction.verify(TESTNET, transaction);
} catch (VerificationException e) {
valid = false;
}