Skip to content

Commit

Permalink
TransactionInput: make field witness immutable
Browse files Browse the repository at this point in the history
Because tweaking is necessary for transaction signing, these usages
have been changed to produce new inputs instead and replace them in
transactions as needed.
  • Loading branch information
schildbach committed Feb 9, 2025
1 parent 0b59788 commit 10af18a
Show file tree
Hide file tree
Showing 7 changed files with 53 additions and 24 deletions.
20 changes: 13 additions & 7 deletions core/src/main/java/org/bitcoinj/core/Transaction.java
Original file line number Diff line number Diff line change
Expand Up @@ -634,8 +634,10 @@ private void readOutputs(ByteBuffer payload) throws BufferUnderflowException, Pr
}

private void readWitnesses(ByteBuffer payload) throws BufferUnderflowException, ProtocolException {
for (TransactionInput input : inputs) {
input.setWitness(TransactionWitness.read(payload));
int numInputs = inputs.size();
for (int i = 0; i < numInputs; i++) {
TransactionWitness witness = TransactionWitness.read(payload);
replaceInput(i, getInput(i).withWitness(witness));
}
}

Expand Down Expand Up @@ -901,18 +903,21 @@ public TransactionInput addSignedInput(TransactionOutPoint prevOut, Script scrip
TransactionSignature signature = calculateSignature(inputIndex, sigKey, scriptPubKey, sigHash,
anyoneCanPay);
input.setScriptSig(ScriptBuilder.createInputScript(signature));
input.setWitness(null);
input = input.withoutWitness();
replaceInput(inputIndex, input);
} else if (ScriptPattern.isP2PKH(scriptPubKey)) {
TransactionSignature signature = calculateSignature(inputIndex, sigKey, scriptPubKey, sigHash,
anyoneCanPay);
input.setScriptSig(ScriptBuilder.createInputScript(signature, sigKey));
input.setWitness(null);
input = input.withoutWitness();
replaceInput(inputIndex, input);
} else if (ScriptPattern.isP2WPKH(scriptPubKey)) {
Script scriptCode = ScriptBuilder.createP2PKHOutputScript(sigKey);
TransactionSignature signature = calculateWitnessSignature(inputIndex, sigKey, scriptCode, input.getValue(),
sigHash, anyoneCanPay);
input.setScriptSig(ScriptBuilder.createEmpty());
input.setWitness(TransactionWitness.redeemP2WPKH(signature, sigKey));
input = input.withWitness(TransactionWitness.redeemP2WPKH(signature, sigKey));
replaceInput(inputIndex, input);
} else {
throw new ScriptException(ScriptError.SCRIPT_ERR_UNKNOWN_ERROR, "Don't know how to sign for this kind of scriptPubKey: " + scriptPubKey);
}
Expand Down Expand Up @@ -1189,9 +1194,10 @@ public Sha256Hash hashForSignature(int inputIndex, byte[] connectedScript, byte
// transaction that step isn't very helpful, but it doesn't add much cost relative to the actual
// EC math so we'll do it anyway.
for (int i = 0; i < tx.inputs.size(); i++) {
TransactionInput input = tx.inputs.get(i);
TransactionInput input = tx.getInput(i);
input.clearScriptBytes();
input.setWitness(null);
input = input.withoutWitness();
tx.replaceInput(i, input);
}

// This step has no purpose beyond being synchronized with Bitcoin Core's bugs. OP_CODESEPARATOR
Expand Down
29 changes: 23 additions & 6 deletions core/src/main/java/org/bitcoinj/core/TransactionInput.java
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ public class TransactionInput {
@Nullable
private Coin value;

private TransactionWitness witness;
private final TransactionWitness witness;

/**
* Creates an input that connects to nothing - used only in creation of coinbase transactions.
Expand Down Expand Up @@ -135,8 +135,8 @@ public TransactionInput(@Nullable Transaction parentTransaction, byte[] scriptBy

/** internal use only */
public TransactionInput(Transaction parentTransaction, byte[] scriptBytes, TransactionOutPoint outpoint,
long sequence, @Nullable Coin value) {
this(Objects.requireNonNull(parentTransaction), null, scriptBytes, outpoint, sequence, value, null);
long sequence, @Nullable Coin value, @Nullable TransactionWitness witness) {
this(Objects.requireNonNull(parentTransaction), null, scriptBytes, outpoint, sequence, value, witness);
}

private TransactionInput(@Nullable Transaction parentTransaction, @Nullable Script scriptSig, byte[] scriptBytes,
Expand Down Expand Up @@ -328,10 +328,27 @@ public TransactionWitness getWitness() {
}

/**
* Set the transaction witness of an input.
* Returns a clone of this input, with a given witness. The typical use-case is transaction signing.
*
* @param witness witness for the clone
* @return clone of input, with given witness
*/
public void setWitness(TransactionWitness witness) {
this.witness = witness;
public TransactionInput withWitness(TransactionWitness witness) {
Objects.requireNonNull(witness);
Script scriptSig = this.scriptSig != null ? this.scriptSig.get() : null;
return new TransactionInput(this.parent, scriptSig, this.scriptBytes, this.outpoint, sequence, this.value,
witness);
}

/**
* Returns a clone of this input, without witness. The typical use-case is transaction signing.
*
* @return clone of input, without witness
*/
public TransactionInput withoutWitness() {
Script scriptSig = this.scriptSig != null ? this.scriptSig.get() : null;
return new TransactionInput(this.parent, scriptSig, this.scriptBytes, this.outpoint, sequence, this.value,
null);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,14 +128,14 @@ public boolean signInputs(ProposedTransaction propTx, KeyBag keyBag) {
inputScript = scriptPubKey.getScriptSigWithSignature(inputScript, signature.encodeToBitcoin(),
sigIndex);
txIn.setScriptSig(inputScript);
txIn.setWitness(null);
txIn = txIn.withoutWitness();
} else if (ScriptPattern.isP2WPKH(scriptPubKey)) {
Script scriptCode = ScriptBuilder.createP2PKHOutputScript(key);
Coin value = txIn.getValue();
TransactionSignature signature = tx.calculateWitnessSignature(i, key, scriptCode, value,
Transaction.SigHash.ALL, false);
txIn.setScriptSig(ScriptBuilder.createEmpty());
txIn.setWitness(TransactionWitness.redeemP2WPKH(signature, key));
txIn = txIn.withWitness(TransactionWitness.redeemP2WPKH(signature, key));
} else {
throw new IllegalStateException(script.toString());
}
Expand All @@ -144,7 +144,7 @@ public boolean signInputs(ProposedTransaction propTx, KeyBag keyBag) {
} catch (ECKey.MissingPrivateKeyException e) {
log.warn("No private key in keypair for input {}", i);
}

tx.replaceInput(i, txIn);
}
return true;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,12 +100,13 @@ public boolean signInputs(ProposedTransaction propTx, KeyBag keyBag) {
} else if (missingSigsMode == Wallet.MissingSigsMode.USE_DUMMY_SIG) {
ECKey key = keyBag.findKeyFromPubKeyHash(
ScriptPattern.extractHashFromP2WH(scriptPubKey), ScriptType.P2WPKH);
txIn.setWitness(TransactionWitness.redeemP2WPKH(TransactionSignature.dummy(), key));
txIn = txIn.withWitness(TransactionWitness.redeemP2WPKH(TransactionSignature.dummy(), key));
}
}
} else {
throw new IllegalStateException("cannot handle: " + scriptPubKey);
}
propTx.partialTx.replaceInput(i, txIn);
}
return true;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -656,16 +656,17 @@ private void readTransaction(Protos.Transaction txProto) throws UnreadableWallet
);
Coin value = inputProto.hasValue() ? Coin.valueOf(inputProto.getValue()) : null;
long sequence = inputProto.hasSequence() ? 0xffffffffL & inputProto.getSequence() : TransactionInput.NO_SEQUENCE;
TransactionInput input = new TransactionInput(tx, scriptBytes, outpoint, sequence, value);
TransactionWitness witness = null;
if (inputProto.hasWitness()) {
Protos.ScriptWitness witnessProto = inputProto.getWitness();
if (witnessProto.getDataCount() > 0) {
List<byte[]> pushes = new ArrayList<>(witnessProto.getDataCount());
for (int j = 0; j < witnessProto.getDataCount(); j++)
pushes.add(witnessProto.getData(j).toByteArray());
input.setWitness(TransactionWitness.of(pushes));
witness = TransactionWitness.of(pushes);
}
}
TransactionInput input = new TransactionInput(tx, scriptBytes, outpoint, sequence, value, witness);
tx.addInput(input);
}

Expand Down
8 changes: 5 additions & 3 deletions core/src/test/java/org/bitcoinj/core/TransactionTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,8 @@ public void testWitnessSignatureP2WPKH() {
assertTrue(correctlySpends(txIn0, scriptPubKey0, 0));

assertFalse(correctlySpends(txIn1, scriptPubKey1, 1));
txIn1.setWitness(TransactionWitness.redeemP2WPKH(txSig1, key1));
txIn1 = txIn1.withWitness(TransactionWitness.redeemP2WPKH(txSig1, key1));
tx.replaceInput(1, txIn1);
// no redeem script for p2wpkh
assertTrue(correctlySpends(txIn1, scriptPubKey1, 1));

Expand Down Expand Up @@ -413,7 +414,8 @@ public void testWitnessSignatureP2SH_P2WPKH() {
ByteUtils.formatHex(txSig.encodeToBitcoin()));

assertFalse(correctlySpends(txIn, scriptPubKey, 0));
txIn.setWitness(TransactionWitness.redeemP2WPKH(txSig, key));
txIn = txIn.withWitness(TransactionWitness.redeemP2WPKH(txSig, key));
tx.replaceInput(0, txIn);
txIn.setScriptSig(new ScriptBuilder().data(redeemScript.program()).build());
assertTrue(correctlySpends(txIn, scriptPubKey, 0));

Expand Down Expand Up @@ -713,7 +715,7 @@ public HugeDeclaredSizeTransaction(boolean hackInputsSize, boolean hackOutputsSi
this.addInput(inputTx.getOutput(0));
this.getInput(0).disconnect();
TransactionWitness witness = TransactionWitness.of(new byte[] { 0 });
this.getInput(0).setWitness(witness);
this.replaceInput(0, this.getInput(0).withWitness(witness));
this.addOutput(Coin.COIN, new ECKey());

this.hackInputsSize = hackInputsSize;
Expand Down
6 changes: 4 additions & 2 deletions core/src/test/java/org/bitcoinj/wallet/WalletTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -3459,7 +3459,8 @@ public void oneTxTwoWallets() {
TransactionSignature txSig1 = sendReq.tx.calculateWitnessSignature(0, sigKey1, scriptCode1,
inputW1.getValue(), Transaction.SigHash.ALL, false);
inputW1.setScriptSig(ScriptBuilder.createEmpty());
inputW1.setWitness(TransactionWitness.redeemP2WPKH(txSig1, sigKey1));
inputW1 = inputW1.withWitness(TransactionWitness.redeemP2WPKH(txSig1, sigKey1));
sendReq.tx.replaceInput(0, inputW1);

// Wallet2 sign input 1
TransactionInput inputW2 = sendReq.tx.getInput(1);
Expand All @@ -3468,7 +3469,8 @@ public void oneTxTwoWallets() {
TransactionSignature txSig2 = sendReq.tx.calculateWitnessSignature(0, sigKey2, scriptCode2,
inputW2.getValue(), Transaction.SigHash.ALL, false);
inputW2.setScriptSig(ScriptBuilder.createEmpty());
inputW2.setWitness(TransactionWitness.redeemP2WPKH(txSig2, sigKey2));
inputW2 = inputW2.withWitness(TransactionWitness.redeemP2WPKH(txSig2, sigKey2));
sendReq.tx.replaceInput(1, inputW2);

wallet1.commitTx(sendReq.tx);
wallet2.commitTx(sendReq.tx);
Expand Down

0 comments on commit 10af18a

Please sign in to comment.