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) { public static Block createGenesis(NetworkParameters n) {
Block genesisBlock = new Block(n, BLOCK_VERSION_GENESIS); Block genesisBlock = new Block(n, BLOCK_VERSION_GENESIS);
Transaction t = createGenesisTransaction(n, genesisTxInputScriptBytes, FIFTY_COINS, genesisTxScriptPubKeyBytes); Transaction tx = Transaction.coinbase(n, genesisTxInputScriptBytes);
genesisBlock.addTransaction(t); tx.addOutput(new TransactionOutput(tx, FIFTY_COINS, genesisTxScriptPubKeyBytes));
genesisBlock.addTransaction(tx);
return genesisBlock; 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: // A script containing the difficulty bits and the following message:
// //
// "The Times 03/Jan/2009 Chancellor on brink of second bailout for banks" // "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 // 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. // counter in the scriptSig so every transaction has a different hash.
coinbase.addInput(new TransactionInput(coinbase, coinbase.addInput(TransactionInput.coinbaseInput(coinbase,
inputBuilder.build().getProgram())); inputBuilder.build().getProgram()));
coinbase.addOutput(new TransactionOutput(coinbase, value, coinbase.addOutput(new TransactionOutput(coinbase, value,
ScriptBuilder.createP2PKOutputScript(ECKey.fromPublicOnly(pubKeyTo)).getProgram())); ScriptBuilder.createP2PKOutputScript(ECKey.fromPublicOnly(pubKeyTo)).getProgram()));

View File

@ -222,6 +222,31 @@ public class Transaction extends Message {
@Nullable @Nullable
private String memo; 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) { public Transaction(NetworkParameters params) {
super(params); super(params);
version = 1; 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. * 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) { public static TransactionInput coinbaseInput(Transaction parentTransaction, byte[] scriptBytes) {
this(parentTransaction, scriptBytes, new TransactionOutPoint(UNCONNECTED, (Transaction) null)); 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, public TransactionInput(@Nullable Transaction parentTransaction, byte[] scriptBytes,

View File

@ -47,6 +47,10 @@ public class TransactionOutPoint extends Message {
static final int MESSAGE_LENGTH = 36; 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. */ /** Hash of the transaction to which we refer. */
private Sha256Hash hash; private Sha256Hash hash;
/** Which output of that transaction we are talking about. */ /** Which output of that transaction we are talking about. */
@ -58,18 +62,13 @@ public class TransactionOutPoint extends Message {
// The connected output. // The connected output.
TransactionOutput connectedOutput; TransactionOutput connectedOutput;
public TransactionOutPoint(long index, @Nullable Transaction fromTx) { public TransactionOutPoint(long index, Transaction fromTx) {
super(); super();
checkArgument(index >= 0 && index <= ByteUtils.MAX_UNSIGNED_INTEGER, () -> checkArgument(index >= 0 && index <= ByteUtils.MAX_UNSIGNED_INTEGER, () ->
"index out of range: " + index); "index out of range: " + index);
this.index = index; this.index = index;
if (fromTx != null) { this.hash = fromTx.getTxId();
this.hash = fromTx.getTxId(); this.fromTx = fromTx;
this.fromTx = fromTx;
} else {
// This happens when constructing the genesis block.
hash = Sha256Hash.ZERO_HASH;
}
} }
public TransactionOutPoint(long index, Sha256Hash hash) { public TransactionOutPoint(long index, Sha256Hash hash) {

View File

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

View File

@ -1007,7 +1007,7 @@ public class FullBlockTestGenerator {
NewBlock b51 = createNextBlock(b44, chainHeadHeight + 16, out15, null); NewBlock b51 = createNextBlock(b44, chainHeadHeight + 16, out15, null);
{ {
Transaction coinbase = new Transaction(params); 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)); coinbase.addOutput(new TransactionOutput(coinbase, SATOSHI, outScriptBytes));
b51.block.addTransaction(coinbase, false); 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.assertEquals;
import static org.junit.Assert.assertNull; import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
public class TransactionInputTest { public class TransactionInputTest {
private static final NetworkParameters TESTNET = TestNet3Params.get(); private static final NetworkParameters TESTNET = TestNet3Params.get();
@ -105,4 +106,10 @@ public class TransactionInputTest {
assertNull(txInToDisconnect.getOutpoint().fromTx); assertNull(txInToDisconnect.getOutpoint().fromTx);
assertNull(txInToDisconnect.getOutpoint().connectedOutput); 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 @Test
public void testToStringWhenIteratingOverAnInputCatchesAnException() { public void testToStringWhenIteratingOverAnInputCatchesAnException() {
Transaction tx = FakeTxBuilder.createFakeTx(TESTNET); Transaction tx = FakeTxBuilder.createFakeTx(TESTNET);
TransactionInput ti = new TransactionInput(tx, new byte[0]) { TransactionInput ti = new TransactionInput(tx, new byte[0], TransactionOutPoint.UNCONNECTED) {
@Override @Override
public Script getScriptSig() throws ScriptException { public Script getScriptSig() throws ScriptException {
throw new ScriptException(ScriptError.SCRIPT_ERR_UNKNOWN_ERROR, ""); 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.Coin;
import org.bitcoinj.base.internal.TimeUtils; import org.bitcoinj.base.internal.TimeUtils;
import org.bitcoinj.core.Context; import org.bitcoinj.core.Context;
import org.bitcoinj.core.TransactionOutPoint;
import org.bitcoinj.crypto.ECKey; import org.bitcoinj.crypto.ECKey;
import org.bitcoinj.core.NetworkParameters; import org.bitcoinj.core.NetworkParameters;
import org.bitcoinj.core.Transaction; import org.bitcoinj.core.Transaction;
@ -90,7 +91,7 @@ public class PaymentSessionTest {
// Send the payment and verify that the correct information is sent. // Send the payment and verify that the correct information is sent.
// Add a dummy input to tx so it is considered valid. // 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<>(); ArrayList<Transaction> txns = new ArrayList<>();
txns.add(tx); txns.add(tx);
Address refundAddr = serverKey.toAddress(ScriptType.P2PKH, BitcoinNetwork.TESTNET); Address refundAddr = serverKey.toAddress(ScriptType.P2PKH, BitcoinNetwork.TESTNET);
@ -130,7 +131,7 @@ public class PaymentSessionTest {
assertTrue(paymentSession.isExpired()); assertTrue(paymentSession.isExpired());
// Send the payment and verify that an exception is thrown. // Send the payment and verify that an exception is thrown.
// Add a dummy input to tx so it is considered valid. // 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<>(); ArrayList<Transaction> txns = new ArrayList<>();
txns.add(tx); txns.add(tx);
@ -169,7 +170,7 @@ public class PaymentSessionTest {
// Send the payment and verify that the correct information is sent. // Send the payment and verify that the correct information is sent.
// Add a dummy input to tx so it is considered valid. // 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<>(); ArrayList<Transaction> txns = new ArrayList<>();
txns.add(tx); txns.add(tx);
Address refundAddr = serverKey.toAddress(ScriptType.P2PKH, BitcoinNetwork.TESTNET); Address refundAddr = serverKey.toAddress(ScriptType.P2PKH, BitcoinNetwork.TESTNET);

View File

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

View File

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

View File

@ -824,7 +824,7 @@ public class PeerTest extends TestWithNetworkConnections {
}); });
connect(); connect();
Transaction t1 = new Transaction(TESTNET); 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)); t1.addOutput(COIN, new ECKey().toAddress(ScriptType.P2PKH, BitcoinNetwork.TESTNET));
Transaction t2 = new Transaction(TESTNET); Transaction t2 = new Transaction(TESTNET);
t2.addInput(t1.getOutput(0)); t2.addInput(t1.getOutput(0));