Transaction, TransactionInput, TransactionOutPoint: add static constructors for the elements of a coinbase

This should reduce misuse of the standard constructors.
This commit is contained in:
Andreas Schildbach 2023-03-31 15:11:28 +02:00
parent 820b671dbc
commit 81fb0c5acb
13 changed files with 73 additions and 43 deletions

View File

@ -253,18 +253,12 @@ public class Block extends Message {
public static Block createGenesis(NetworkParameters n) {
Block genesisBlock = new Block(n, BLOCK_VERSION_GENESIS);
Transaction t = createGenesisTransaction(n, genesisTxInputScriptBytes, FIFTY_COINS, genesisTxScriptPubKeyBytes);
genesisBlock.addTransaction(t);
Transaction tx = Transaction.coinbase(n, genesisTxInputScriptBytes);
tx.addOutput(new TransactionOutput(tx, FIFTY_COINS, genesisTxScriptPubKeyBytes));
genesisBlock.addTransaction(tx);
return genesisBlock;
}
private static Transaction createGenesisTransaction(NetworkParameters n, byte[] inputScriptBytes, Coin amount, byte[] scriptPubKeyBytes) {
Transaction t = new Transaction(n);
t.addInput(new TransactionInput(t, inputScriptBytes));
t.addOutput(new TransactionOutput(t, amount, scriptPubKeyBytes));
return t;
}
// A script containing the difficulty bits and the following message:
//
// "The Times 03/Jan/2009 Chancellor on brink of second bailout for banks"
@ -891,7 +885,7 @@ public class Block extends Message {
//
// Here we will do things a bit differently so a new address isn't needed every time. We'll put a simple
// counter in the scriptSig so every transaction has a different hash.
coinbase.addInput(new TransactionInput(coinbase,
coinbase.addInput(TransactionInput.coinbaseInput(coinbase,
inputBuilder.build().getProgram()));
coinbase.addOutput(new TransactionOutput(coinbase, value,
ScriptBuilder.createP2PKOutputScript(ECKey.fromPublicOnly(pubKeyTo)).getProgram()));

View File

@ -222,6 +222,31 @@ public class Transaction extends Message {
@Nullable
private String memo;
/**
* Constructs an incomplete coinbase transaction with a minimal input script and no outputs.
*
* @param params network to use
* @return coinbase transaction
*/
public static Transaction coinbase(NetworkParameters params) {
Transaction tx = new Transaction(params);
tx.addInput(TransactionInput.coinbaseInput(tx, new byte[2])); // 2 is minimum
return tx;
}
/**
* Constructs an incomplete coinbase transaction with given bytes for the input script and no outputs.
*
* @param params network to use
* @param inputScriptBytes arbitrary bytes for the coinbase input
* @return coinbase transaction
*/
public static Transaction coinbase(NetworkParameters params, byte[] inputScriptBytes) {
Transaction tx = new Transaction(params);
tx.addInput(TransactionInput.coinbaseInput(tx, inputScriptBytes));
return tx;
}
public Transaction(NetworkParameters params) {
super(params);
version = 1;

View File

@ -92,9 +92,15 @@ public class TransactionInput extends Message {
/**
* Creates an input that connects to nothing - used only in creation of coinbase transactions.
*
* @param parentTransaction parent transaction
* @param scriptBytes arbitrary bytes in the script
*/
public TransactionInput(@Nullable Transaction parentTransaction, byte[] scriptBytes) {
this(parentTransaction, scriptBytes, new TransactionOutPoint(UNCONNECTED, (Transaction) null));
public static TransactionInput coinbaseInput(Transaction parentTransaction, byte[] scriptBytes) {
Objects.requireNonNull(parentTransaction);
checkArgument(scriptBytes.length >= 2 && scriptBytes.length <= 100, () ->
"script must be between 2 and 100 bytes: " + scriptBytes.length);
return new TransactionInput(parentTransaction, scriptBytes, TransactionOutPoint.UNCONNECTED);
}
public TransactionInput(@Nullable Transaction parentTransaction, byte[] scriptBytes,

View File

@ -47,6 +47,10 @@ public class TransactionOutPoint extends Message {
static final int MESSAGE_LENGTH = 36;
/** Special outpoint that normally marks a coinbase input. It's also used as a test dummy. */
public static final TransactionOutPoint UNCONNECTED =
new TransactionOutPoint(ByteUtils.MAX_UNSIGNED_INTEGER, Sha256Hash.ZERO_HASH);
/** Hash of the transaction to which we refer. */
private Sha256Hash hash;
/** Which output of that transaction we are talking about. */
@ -58,18 +62,13 @@ public class TransactionOutPoint extends Message {
// The connected output.
TransactionOutput connectedOutput;
public TransactionOutPoint(long index, @Nullable Transaction fromTx) {
public TransactionOutPoint(long index, Transaction fromTx) {
super();
checkArgument(index >= 0 && index <= ByteUtils.MAX_UNSIGNED_INTEGER, () ->
"index out of range: " + index);
this.index = index;
if (fromTx != null) {
this.hash = fromTx.getTxId();
this.fromTx = fromTx;
} else {
// This happens when constructing the genesis block.
hash = Sha256Hash.ZERO_HASH;
}
this.hash = fromTx.getTxId();
this.fromTx = fromTx;
}
public TransactionOutPoint(long index, Sha256Hash hash) {

View File

@ -74,14 +74,10 @@ public class FakeTxBuilder {
/** Create a fake coinbase transaction. */
public static Transaction createFakeCoinbaseTx(final NetworkParameters params) {
TransactionOutPoint outpoint = new TransactionOutPoint(ByteUtils.MAX_UNSIGNED_INTEGER, Sha256Hash.ZERO_HASH);
TransactionInput input = new TransactionInput(null, new byte[0], outpoint);
Transaction tx = new Transaction(params);
tx.addInput(input);
Transaction tx = Transaction.coinbase(params);
TransactionOutput outputToMe = new TransactionOutput(tx, Coin.FIFTY_COINS, randomAddress(params));
tx.addOutput(outputToMe);
checkState(tx.isCoinBase());
tx.addOutput(outputToMe);
return tx;
}

View File

@ -1007,7 +1007,7 @@ public class FullBlockTestGenerator {
NewBlock b51 = createNextBlock(b44, chainHeadHeight + 16, out15, null);
{
Transaction coinbase = new Transaction(params);
coinbase.addInput(new TransactionInput(coinbase, new byte[]{(byte) 0xff, 110, 1}));
coinbase.addInput(TransactionInput.coinbaseInput(coinbase, new byte[]{(byte) 0xff, 110, 1}));
coinbase.addOutput(new TransactionOutput(coinbase, SATOSHI, outScriptBytes));
b51.block.addTransaction(coinbase, false);
}

View File

@ -36,6 +36,7 @@ import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
public class TransactionInputTest {
private static final NetworkParameters TESTNET = TestNet3Params.get();
@ -105,4 +106,10 @@ public class TransactionInputTest {
assertNull(txInToDisconnect.getOutpoint().fromTx);
assertNull(txInToDisconnect.getOutpoint().connectedOutput);
}
@Test
public void coinbaseInput() {
TransactionInput coinbaseInput = TransactionInput.coinbaseInput(new Transaction(TESTNET), new byte[2]);
assertTrue(coinbaseInput.isCoinBase());
}
}

View File

@ -489,7 +489,7 @@ public class TransactionTest {
@Test
public void testToStringWhenIteratingOverAnInputCatchesAnException() {
Transaction tx = FakeTxBuilder.createFakeTx(TESTNET);
TransactionInput ti = new TransactionInput(tx, new byte[0]) {
TransactionInput ti = new TransactionInput(tx, new byte[0], TransactionOutPoint.UNCONNECTED) {
@Override
public Script getScriptSig() throws ScriptException {
throw new ScriptException(ScriptError.SCRIPT_ERR_UNKNOWN_ERROR, "");

View File

@ -25,6 +25,7 @@ import org.bitcoinj.base.Address;
import org.bitcoinj.base.Coin;
import org.bitcoinj.base.internal.TimeUtils;
import org.bitcoinj.core.Context;
import org.bitcoinj.core.TransactionOutPoint;
import org.bitcoinj.crypto.ECKey;
import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.core.Transaction;
@ -90,7 +91,7 @@ public class PaymentSessionTest {
// Send the payment and verify that the correct information is sent.
// Add a dummy input to tx so it is considered valid.
tx.addInput(new TransactionInput(tx, outputToMe.getScriptBytes()));
tx.addInput(new TransactionInput(tx, outputToMe.getScriptBytes(), TransactionOutPoint.UNCONNECTED));
ArrayList<Transaction> txns = new ArrayList<>();
txns.add(tx);
Address refundAddr = serverKey.toAddress(ScriptType.P2PKH, BitcoinNetwork.TESTNET);
@ -130,7 +131,7 @@ public class PaymentSessionTest {
assertTrue(paymentSession.isExpired());
// Send the payment and verify that an exception is thrown.
// Add a dummy input to tx so it is considered valid.
tx.addInput(new TransactionInput(tx, outputToMe.getScriptBytes()));
tx.addInput(new TransactionInput(tx, outputToMe.getScriptBytes(), TransactionOutPoint.UNCONNECTED));
ArrayList<Transaction> txns = new ArrayList<>();
txns.add(tx);
@ -169,7 +170,7 @@ public class PaymentSessionTest {
// Send the payment and verify that the correct information is sent.
// Add a dummy input to tx so it is considered valid.
tx.addInput(new TransactionInput(tx, outputToMe.getScriptBytes()));
tx.addInput(new TransactionInput(tx, outputToMe.getScriptBytes(), TransactionOutPoint.UNCONNECTED));
ArrayList<Transaction> txns = new ArrayList<>();
txns.add(tx);
Address refundAddr = serverKey.toAddress(ScriptType.P2PKH, BitcoinNetwork.TESTNET);

View File

@ -248,7 +248,7 @@ public class ScriptTest {
public void testOp0() {
// Check that OP_0 doesn't NPE and pushes an empty stack frame.
Transaction tx = new Transaction(TESTNET);
tx.addInput(new TransactionInput(tx, new byte[] {}));
tx.addInput(new TransactionInput(tx, new byte[0], TransactionOutPoint.UNCONNECTED));
Script script = new ScriptBuilder().smallNum(0).build();
LinkedList<byte[]> stack = new LinkedList<>();
@ -354,7 +354,7 @@ public class ScriptTest {
tx.setLockTime(0);
TransactionInput txInput = new TransactionInput(null,
new ScriptBuilder().number(0).number(0).build().getProgram());
new ScriptBuilder().number(0).number(0).build().getProgram(), TransactionOutPoint.UNCONNECTED);
txInput.setSequenceNumber(TransactionInput.NO_SEQUENCE);
tx.addInput(txInput);
@ -369,7 +369,8 @@ public class ScriptTest {
tx.setVersion(1);
tx.setLockTime(0);
TransactionInput txInput = new TransactionInput(creditingTransaction, scriptSig.getProgram());
TransactionInput txInput = new TransactionInput(creditingTransaction, scriptSig.getProgram(),
TransactionOutPoint.UNCONNECTED);
txInput.setSequenceNumber(TransactionInput.NO_SEQUENCE);
tx.addInput(txInput);

View File

@ -21,6 +21,7 @@ import org.bitcoinj.base.Coin;
import org.bitcoinj.base.ScriptType;
import org.bitcoinj.base.internal.ByteUtils;
import org.bitcoinj.core.Context;
import org.bitcoinj.core.TransactionOutPoint;
import org.bitcoinj.crypto.ECKey;
import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.core.Transaction;
@ -161,7 +162,7 @@ public class DefaultRiskAnalysisTest {
// Test non-standard script as an input.
Transaction tx = new Transaction(MAINNET);
assertEquals(DefaultRiskAnalysis.RuleViolation.NONE, DefaultRiskAnalysis.isStandard(tx));
tx.addInput(new TransactionInput(null, nonStandardScript));
tx.addInput(new TransactionInput(null, nonStandardScript, TransactionOutPoint.UNCONNECTED));
assertEquals(DefaultRiskAnalysis.RuleViolation.SHORTEST_POSSIBLE_PUSHDATA, DefaultRiskAnalysis.isStandard(tx));
// Test non-standard script as an output.
tx.clearInputs();
@ -175,15 +176,14 @@ public class DefaultRiskAnalysisTest {
TransactionSignature sig = TransactionSignature.dummy();
Script scriptOk = ScriptBuilder.createInputScript(sig);
assertEquals(RuleViolation.NONE,
DefaultRiskAnalysis.isInputStandard(new TransactionInput(null, scriptOk.getProgram())));
DefaultRiskAnalysis.isInputStandard(new TransactionInput(null, scriptOk.getProgram(), TransactionOutPoint.UNCONNECTED)));
byte[] sigBytes = sig.encodeToBitcoin();
// Appending a zero byte makes the signature uncanonical without violating DER encoding.
Script scriptUncanonicalEncoding = new ScriptBuilder().data(Arrays.copyOf(sigBytes, sigBytes.length + 1))
.build();
assertEquals(RuleViolation.SIGNATURE_CANONICAL_ENCODING,
DefaultRiskAnalysis.isInputStandard(new TransactionInput(null, scriptUncanonicalEncoding
.getProgram())));
assertEquals(RuleViolation.SIGNATURE_CANONICAL_ENCODING, DefaultRiskAnalysis.isInputStandard(
new TransactionInput(null, scriptUncanonicalEncoding.getProgram(), TransactionOutPoint.UNCONNECTED)));
}
@Test
@ -192,8 +192,8 @@ public class DefaultRiskAnalysisTest {
TransactionSignature sig = TransactionSignature.dummy();
Script scriptHighS = ScriptBuilder
.createInputScript(new TransactionSignature(sig.r, ECKey.CURVE.getN().subtract(sig.s)));
assertEquals(RuleViolation.SIGNATURE_CANONICAL_ENCODING,
DefaultRiskAnalysis.isInputStandard(new TransactionInput(null, scriptHighS.getProgram())));
assertEquals(RuleViolation.SIGNATURE_CANONICAL_ENCODING, DefaultRiskAnalysis.isInputStandard(
new TransactionInput(null, scriptHighS.getProgram(), TransactionOutPoint.UNCONNECTED)));
// This is a real transaction. Its signatures S component is "low".
Transaction tx1 = new Transaction(MAINNET, ByteBuffer.wrap(ByteUtils.parseHex(

View File

@ -751,7 +751,8 @@ public class WalletTest extends TestWithWallet {
TransactionOutput to = createMock(TransactionOutput.class);
EasyMock.expect(to.isAvailableForSpending()).andReturn(true);
EasyMock.expect(to.isMineOrWatched(wallet)).andReturn(true);
EasyMock.expect(to.getSpentBy()).andReturn(new TransactionInput(null, new byte[0]));
EasyMock.expect(to.getSpentBy()).andReturn(
new TransactionInput(null, new byte[0], TransactionOutPoint.UNCONNECTED));
Transaction tx = FakeTxBuilder.createFakeTxWithoutChange(TESTNET, to);

View File

@ -824,7 +824,7 @@ public class PeerTest extends TestWithNetworkConnections {
});
connect();
Transaction t1 = new Transaction(TESTNET);
t1.addInput(new TransactionInput(t1, new byte[]{}));
t1.addInput(new TransactionInput(t1, new byte[0], TransactionOutPoint.UNCONNECTED));
t1.addOutput(COIN, new ECKey().toAddress(ScriptType.P2PKH, BitcoinNetwork.TESTNET));
Transaction t2 = new Transaction(TESTNET);
t2.addInput(t1.getOutput(0));