Skip to content

Commit 35b331e

Browse files
authored
Merge pull request #20 from twostack/signature-bugfix
Second Signature Bugfix
2 parents e524cfb + 3536482 commit 35b331e

File tree

4 files changed

+88
-17
lines changed

4 files changed

+88
-17
lines changed

CHANGELOG.md

+7
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
#Release 1.6.4
2+
### Second BugFix for Signature Generation
3+
4+
- When spending multiple outputs from the same transaction it was possible
5+
for some of the spending inputs to not get signed.
6+
This update fixes this bug.
7+
18
#Release 1.6.3
29
### BugFix for Signature Generation
310

build.gradle

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ plugins {
66
}
77

88
group 'org.twostack'
9-
version '1.6.3'
9+
version '1.6.4'
1010

1111
repositories {
1212
mavenCentral()

src/main/java/org/twostack/bitcoin4j/transaction/TransactionBuilder.java

+7-7
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ public TransactionBuilder spendFromUtxoMap(TransactionSigner signer, Map<String,
116116
outpoint.setSatoshis((BigInteger)utxoMap.get("satoshis"));
117117
outpoint.setTransactionId(transactionId);
118118

119-
this.signerMap.put(transactionId, new SignerDto(signer, outpoint));
119+
this.signerMap.put(transactionId + ":" + outputIndex, new SignerDto(signer, outpoint));
120120

121121
if (unlocker == null){
122122
unlocker = new DefaultUnlockBuilder();
@@ -186,7 +186,7 @@ public TransactionBuilder spendFromTransaction(TransactionSigner signer, Transac
186186
outpoint.setSatoshis(output.getAmount());
187187
outpoint.setTransactionId(transactionId);
188188

189-
this.signerMap.put(transactionId, new SignerDto(signer, outpoint));
189+
this.signerMap.put(transactionId + ":" + outputIndex, new SignerDto(signer, outpoint));
190190

191191
//update the spending transactionInput
192192
TransactionInput input = new TransactionInput(
@@ -220,7 +220,7 @@ public TransactionBuilder spendFromTransaction(Transaction txn, int outputIndex,
220220

221221
public TransactionBuilder spendFromOutpoint(TransactionSigner signer, TransactionOutpoint outpoint, long sequenceNumber, UnlockingScriptBuilder unlocker) {
222222

223-
this.signerMap.put(outpoint.getTransactionId(), new SignerDto(signer, outpoint));
223+
this.signerMap.put(outpoint.getTransactionId() + ":" + outpoint.getOutputIndex(), new SignerDto(signer, outpoint));
224224

225225
TransactionInput input = new TransactionInput(
226226
HEX.decode(outpoint.getTransactionId()),
@@ -275,7 +275,7 @@ public TransactionBuilder spendFromOutput(TransactionSigner signer, String utxoT
275275
outpoint.setSatoshis(amount);
276276
outpoint.setTransactionId(utxoTxnId);
277277

278-
this.signerMap.put(utxoTxnId, new SignerDto(signer, outpoint));
278+
this.signerMap.put(utxoTxnId + ":" + outputIndex, new SignerDto(signer, outpoint));
279279

280280
TransactionInput input = new TransactionInput(
281281
HEX.decode(utxoTxnId),
@@ -434,9 +434,9 @@ public Transaction build(boolean performChecks) throws TransactionException, IOE
434434
TransactionInput currentInput = inputs.get(index);
435435

436436
List<Map.Entry<String, SignerDto>> result = signerMap.entrySet().stream().filter( (Map.Entry<String, SignerDto> entry) -> {
437-
//drop everything from stream except what we're looking for
438-
return (entry.getValue().outpoint.getTransactionId().equals(HEX.encode(currentInput.getPrevTxnId())) &&
439-
entry.getValue().outpoint.getOutputIndex() == currentInput.getPrevTxnOutputIndex());
437+
String entryKey = entry.getValue().outpoint.getTransactionId() + ":" + entry.getValue().outpoint.getOutputIndex();
438+
String currentInputKey = Utils.HEX.encode(currentInput.getPrevTxnId()) + ":" + currentInput.getPrevTxnOutputIndex();
439+
return entryKey.equals(currentInputKey);
440440
}).collect(Collectors.toList());
441441

442442
if (result.size() > 0) {

src/test/java/org/twostack/bitcoin4j/transaction/TransactionBuilderTest.java

+73-9
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,20 @@
88
import org.twostack.bitcoin4j.Coin;
99
import org.twostack.bitcoin4j.PrivateKey;
1010
import org.twostack.bitcoin4j.Utils;
11-
import org.twostack.bitcoin4j.exception.InvalidKeyException;
12-
import org.twostack.bitcoin4j.exception.SigHashException;
13-
import org.twostack.bitcoin4j.exception.SignatureDecodeException;
14-
import org.twostack.bitcoin4j.exception.TransactionException;
11+
import org.twostack.bitcoin4j.crypto.*;
12+
import org.twostack.bitcoin4j.exception.*;
1513
import org.twostack.bitcoin4j.params.NetworkAddressType;
14+
import org.twostack.bitcoin4j.params.NetworkType;
1615
import org.twostack.bitcoin4j.script.Interpreter;
1716
import org.twostack.bitcoin4j.script.Script;
1817

1918
import java.io.IOException;
2019
import java.io.InputStreamReader;
2120
import java.math.BigInteger;
2221
import java.nio.charset.StandardCharsets;
23-
import java.util.Arrays;
24-
import java.util.HashMap;
25-
import java.util.HashSet;
26-
import java.util.Set;
22+
import java.util.*;
23+
24+
import static org.twostack.bitcoin4j.Utils.WHITESPACE_SPLITTER;
2725

2826
public class TransactionBuilderTest {
2927

@@ -270,7 +268,7 @@ public void builderCanSpendFromUtxoMap() throws InvalidKeyException, IOException
270268
UnlockingScriptBuilder unlocker = new P2PKHUnlockBuilder(privateKey.getPublicKey());
271269

272270
TransactionSigner signer = new TransactionSigner(SigHashType.ALL.value | SigHashType.FORKID.value, privateKey);
273-
builder.spendFromUtxoMap(signer, utxoMap, unlocker);
271+
builder.spendFromUtxoMap(signer, utxoMap, unlocker);
274272

275273
}
276274

@@ -308,4 +306,70 @@ public void builderCanSpendFromUtxoMap() throws InvalidKeyException, IOException
308306
}
309307
}).doesNotThrowAnyException();
310308
}
309+
310+
@Test
311+
public void canSpendMultipleOutputsFromSameTx() throws MnemonicException, IOException, InvalidKeyException, TransactionException, SigHashException, SignatureDecodeException {
312+
313+
PrivateKey pkOne = PrivateKey.fromWIF("L14C38sujYefjpj4Zj5HTwjaHAZKMSLZZAnzLNojC8CmU8Q4TpWE");
314+
PrivateKey pkTwo = PrivateKey.fromWIF("L4YDdzJa5Ae2ukENpPqFBPCutM1xsM9WvKFy6Bugv4iyg6U4c2kE");
315+
316+
TransactionBuilder builder = new TransactionBuilder();
317+
builder.withFeePerKb(512);
318+
319+
String rawFundingTx = "0100000001c5d4b2f482627e7c46ad977cbacf9bfdf2197229952daa3a4b57d15fccfad92b000000006a47304402207927136ea0b51fc9a2cb883ac2a72410dec41bef98062b7845eac691cc2c9f6602202b8b370b588542941623379a046e5aea654b1edd5c9c074a426b1be45545ed5d412103c36ea9ccb9a332b415ebf9e9823e2b352f2ed2c4199ea382cf98ddbfcf4eed24ffffffff02204e0000000000001976a914eb6edc362ae7e5d2765e86a97741722b0f7e20d688ac260c0300000000001976a914cd1a22818d1f143b152276ee6a69486233405d3e88ac00000000";
320+
Transaction fundingTx = Transaction.fromHex(rawFundingTx);
321+
322+
P2PKHUnlockBuilder unlockOne = new P2PKHUnlockBuilder(pkOne.getPublicKey());
323+
TransactionSigner signerOne = new TransactionSigner(SigHashType.ALL.value | SigHashType.FORKID.value, pkOne);
324+
builder.spendFromTransaction(signerOne, fundingTx, 0, TransactionInput.MAX_SEQ_NUMBER, unlockOne);
325+
326+
P2PKHUnlockBuilder unlockTwo = new P2PKHUnlockBuilder(pkTwo.getPublicKey());
327+
TransactionSigner signerTwo = new TransactionSigner(SigHashType.ALL.value | SigHashType.FORKID.value, pkTwo);
328+
builder.spendFromTransaction(signerTwo, fundingTx, 1, TransactionInput.MAX_SEQ_NUMBER, unlockTwo);
329+
330+
//send back to the first key
331+
Address recipientAddress = Address.fromKey(NetworkAddressType.MAIN_PKH, pkOne.getPublicKey());
332+
P2PKHLockBuilder lockBuilder = new P2PKHLockBuilder(recipientAddress);
333+
builder.spendTo(lockBuilder, BigInteger.valueOf(20000));
334+
335+
//send change to second key
336+
Address changeAddress = Address.fromKey(NetworkAddressType.MAIN_PKH, pkTwo.getPublicKey());
337+
builder.sendChangeTo(changeAddress);
338+
339+
Transaction broadcastTx = builder.build(true);
340+
341+
Assertions.assertThatCode(() -> {
342+
343+
//new Script Interpreter to help us verify our spending conditions
344+
Interpreter interp = new Interpreter();
345+
Set<Script.VerifyFlag> verifyFlags = new HashSet<Script.VerifyFlag>(Arrays.asList(Script.VerifyFlag.SIGHASH_FORKID));
346+
347+
348+
// ////FIRST UTXO
349+
Integer fundingOutputIndexOne = 0;
350+
Long fundingValueOne = 20000L;
351+
352+
//lookup funding transaction corresponding to first output
353+
TransactionOutput fundingOutput = fundingTx.getOutputs().get(fundingOutputIndexOne);
354+
355+
//assert that funding transaction is spending correctly
356+
interp.correctlySpends(broadcastTx.getInputs().get(0).getScriptSig(), fundingOutput.getScript(), broadcastTx, 0, verifyFlags, Coin.valueOf(fundingValueOne));
357+
358+
////SECOND UTXO
359+
// TransactionInput spendingInputTwo = TransactionInput.fromByteArray(broadcastTx.getInputs().get(1).serialize());
360+
Integer fundingOutputIndexTwo = 1;
361+
Long fundingValueTwo = 199718L;
362+
363+
//lookup funding transaction corresponding to first output
364+
TransactionOutput fundingOutputTwo = fundingTx.getOutputs().get(fundingOutputIndexTwo);
365+
366+
Transaction signedTxTwo = signerTwo.sign(broadcastTx, fundingOutputTwo, 1);
367+
368+
//assert that funding transaction is spending correctly
369+
interp.correctlySpends(broadcastTx.getInputs().get(1).getScriptSig(), fundingOutputTwo.getScript(), broadcastTx, 1, verifyFlags, Coin.valueOf(fundingValueTwo));
370+
371+
}).doesNotThrowAnyException();
372+
373+
}
374+
311375
}

0 commit comments

Comments
 (0)