Skip to content

Commit 8c59532

Browse files
authored
Merge pull request #19 from twostack/txbuilder-api
BugFix for Signature Generation
2 parents 42344b8 + 95920f4 commit 8c59532

File tree

5 files changed

+150
-16
lines changed

5 files changed

+150
-16
lines changed

CHANGELOG.md

+8
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
#Release 1.6.3
2+
### BugFix for Signature Generation
3+
4+
- Bug was introduced by previous feature where spending and signing
5+
multiple inputs resulted in invalid signatures being created. Fixed.
6+
- Added additional testing via the Script Interpreter to verify that
7+
signatures don't break and that utxos are spent correctly.
8+
19
#Release 1.6.2
210
### Foot-in-mouth release
311

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.2'
9+
version '1.6.3'
1010

1111
repositories {
1212
mavenCentral()

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

+13-14
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import java.io.IOException;
2828
import java.math.BigInteger;
2929
import java.util.*;
30+
import java.util.stream.Collectors;
3031
import java.util.stream.Stream;
3132

3233
import static org.twostack.bitcoin4j.Utils.HEX;
@@ -421,20 +422,26 @@ public Transaction build(boolean performChecks) throws TransactionException, IOE
421422
//add transaction outputs
422423
tx.addOutputs(outputs);
423424

425+
if (changeScriptBuilder != null) {
426+
tx.addOutput(getChangeOutput());
427+
}
428+
429+
tx.setLockTime(nLockTime);
430+
424431
//update inputs with signatures
425-
String txId = tx.getTransactionId();
432+
// String txId = tx.getTransactionId();
426433
for (int index = 0; index < inputs.size() ; index++) {
427434
TransactionInput currentInput = inputs.get(index);
428435

429-
Optional<Map.Entry<String, SignerDto>> result = signerMap.entrySet().stream().filter( (Map.Entry<String, SignerDto> entry) -> {
436+
List<Map.Entry<String, SignerDto>> result = signerMap.entrySet().stream().filter( (Map.Entry<String, SignerDto> entry) -> {
430437
//drop everything from stream except what we're looking for
431-
return !(entry.getValue().outpoint.getTransactionId() == HEX.encode(currentInput.getPrevTxnId()) &&
438+
return (entry.getValue().outpoint.getTransactionId().equals(HEX.encode(currentInput.getPrevTxnId())) &&
432439
entry.getValue().outpoint.getOutputIndex() == currentInput.getPrevTxnOutputIndex());
433-
}).findFirst();
440+
}).collect(Collectors.toList());
434441

435-
if (result.isPresent()) {
442+
if (result.size() > 0) {
436443

437-
SignerDto dto = result.get().getValue();
444+
SignerDto dto = result.get(0).getValue();
438445
TransactionOutput utxoToSpend = new TransactionOutput(dto.outpoint.getSatoshis(), dto.outpoint.getLockingScript());
439446

440447
//TODO: this side-effect programming where the signer mutates my local variable
@@ -443,14 +450,6 @@ public Transaction build(boolean performChecks) throws TransactionException, IOE
443450
}
444451
}
445452

446-
447-
448-
if (changeScriptBuilder != null) {
449-
tx.addOutput(getChangeOutput());
450-
}
451-
452-
tx.setLockTime(nLockTime);
453-
454453
return tx;
455454

456455
}

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

+105-1
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,25 @@
55
import org.assertj.core.api.Assertions;
66
import org.junit.Test;
77
import org.twostack.bitcoin4j.Address;
8+
import org.twostack.bitcoin4j.Coin;
89
import org.twostack.bitcoin4j.PrivateKey;
910
import org.twostack.bitcoin4j.Utils;
1011
import org.twostack.bitcoin4j.exception.InvalidKeyException;
1112
import org.twostack.bitcoin4j.exception.SigHashException;
1213
import org.twostack.bitcoin4j.exception.SignatureDecodeException;
1314
import org.twostack.bitcoin4j.exception.TransactionException;
1415
import org.twostack.bitcoin4j.params.NetworkAddressType;
16+
import org.twostack.bitcoin4j.script.Interpreter;
17+
import org.twostack.bitcoin4j.script.Script;
1518

1619
import java.io.IOException;
1720
import java.io.InputStreamReader;
1821
import java.math.BigInteger;
1922
import java.nio.charset.StandardCharsets;
23+
import java.util.Arrays;
2024
import java.util.HashMap;
25+
import java.util.HashSet;
26+
import java.util.Set;
2127

2228
public class TransactionBuilderTest {
2329

@@ -55,10 +61,35 @@ public void processAndSignMultiInput() throws IOException, InvalidKeyException,
5561

5662

5763
Assertions.assertThatCode(() -> {
58-
Transaction broadcastTx = builder.withFeePerKb(512)
64+
Transaction broadcastTx = builder.withFeePerKb(1024)
5965
.spendTo(new P2PKHLockBuilder(recipientAddress), BigInteger.valueOf(100000))
6066
.sendChangeTo(recipientAddress)
6167
.build(true);
68+
69+
//new Script Interpreter to help us verify our spending conditions
70+
Interpreter interp = new Interpreter();
71+
72+
//loop over every one of our spending inputs and verify we are
73+
//correctly spending those outputs
74+
for (int scriptSigIndex = 0; scriptSigIndex < 0; scriptSigIndex++) {
75+
TransactionInput spendingInputOne = TransactionInput.fromByteArray(broadcastTx.getInputs().get(0).serialize());
76+
String fundingTxId = Utils.HEX.encode(spendingInputOne.getPrevTxnId());
77+
Integer fundingOutputIndex = json.get(scriptSigIndex).get("tx_pos").asInt();
78+
Long fundingValue = json.get(scriptSigIndex).get("value").asLong();
79+
80+
//lookup funding transaction corresponding to first output
81+
String rawFundingTx = json.get(scriptSigIndex).get("raw_tx").asText();
82+
Transaction fundingTxOne = Transaction.fromHex(rawFundingTx);
83+
TransactionOutput fundingOutput = fundingTxOne.getOutputs().get(fundingOutputIndex);
84+
85+
TransactionSigner signer = new TransactionSigner(SigHashType.FORKID.value | SigHashType.ALL.value, privateKey);
86+
Transaction signedTx = signer.sign(broadcastTx, fundingOutput, scriptSigIndex);
87+
88+
//assert that funding transaction is spending correctly
89+
Set<Script.VerifyFlag> verifyFlags = new HashSet<Script.VerifyFlag>(Arrays.asList(Script.VerifyFlag.SIGHASH_FORKID));
90+
interp.correctlySpends(signedTx.getInputs().get(scriptSigIndex).getScriptSig(), fundingOutput.getScript(), broadcastTx, scriptSigIndex, verifyFlags, Coin.valueOf(fundingValue));
91+
}
92+
6293
}).doesNotThrowAnyException();
6394

6495
}
@@ -101,6 +132,30 @@ public void builderCanSpendFromOutput() throws InvalidKeyException, IOException
101132
.spendTo(new P2PKHLockBuilder(recipientAddress), BigInteger.valueOf(100000))
102133
.sendChangeTo(recipientAddress)
103134
.build(true);
135+
136+
//new Script Interpreter to help us verify our spending conditions
137+
Interpreter interp = new Interpreter();
138+
139+
//loop over every one of our spending inputs and verify we are
140+
//correctly spending those outputs
141+
for (int scriptSigIndex = 0; scriptSigIndex < 0; scriptSigIndex++) {
142+
TransactionInput spendingInputOne = TransactionInput.fromByteArray(broadcastTx.getInputs().get(0).serialize());
143+
String fundingTxId = Utils.HEX.encode(spendingInputOne.getPrevTxnId());
144+
Integer fundingOutputIndex = json.get(scriptSigIndex).get("tx_pos").asInt();
145+
Long fundingValue = json.get(scriptSigIndex).get("value").asLong();
146+
147+
//lookup funding transaction corresponding to first output
148+
String rawFundingTx = json.get(scriptSigIndex).get("raw_tx").asText();
149+
Transaction fundingTxOne = Transaction.fromHex(rawFundingTx);
150+
TransactionOutput fundingOutput = fundingTxOne.getOutputs().get(fundingOutputIndex);
151+
152+
TransactionSigner signer = new TransactionSigner(SigHashType.FORKID.value | SigHashType.ALL.value, privateKey);
153+
Transaction signedTx = signer.sign(broadcastTx, fundingOutput, scriptSigIndex);
154+
155+
//assert that funding transaction is spending correctly
156+
Set<Script.VerifyFlag> verifyFlags = new HashSet<Script.VerifyFlag>(Arrays.asList(Script.VerifyFlag.SIGHASH_FORKID));
157+
interp.correctlySpends(signedTx.getInputs().get(scriptSigIndex).getScriptSig(), fundingOutput.getScript(), broadcastTx, scriptSigIndex, verifyFlags, Coin.valueOf(fundingValue));
158+
}
104159
}).doesNotThrowAnyException();
105160

106161
}
@@ -152,6 +207,30 @@ public void builderCanSpendFromOutpoint() throws InvalidKeyException, IOExceptio
152207
.spendTo(new P2PKHLockBuilder(recipientAddress), BigInteger.valueOf(100000))
153208
.sendChangeTo(recipientAddress)
154209
.build(true);
210+
211+
//new Script Interpreter to help us verify our spending conditions
212+
Interpreter interp = new Interpreter();
213+
214+
//loop over every one of our spending inputs and verify we are
215+
//correctly spending those outputs
216+
for (int scriptSigIndex = 0; scriptSigIndex < 0; scriptSigIndex++) {
217+
TransactionInput spendingInputOne = TransactionInput.fromByteArray(broadcastTx.getInputs().get(0).serialize());
218+
String fundingTxId = Utils.HEX.encode(spendingInputOne.getPrevTxnId());
219+
Integer fundingOutputIndex = json.get(scriptSigIndex).get("tx_pos").asInt();
220+
Long fundingValue = json.get(scriptSigIndex).get("value").asLong();
221+
222+
//lookup funding transaction corresponding to first output
223+
String rawFundingTx = json.get(scriptSigIndex).get("raw_tx").asText();
224+
Transaction fundingTxOne = Transaction.fromHex(rawFundingTx);
225+
TransactionOutput fundingOutput = fundingTxOne.getOutputs().get(fundingOutputIndex);
226+
227+
TransactionSigner signer = new TransactionSigner(SigHashType.FORKID.value | SigHashType.ALL.value, privateKey);
228+
Transaction signedTx = signer.sign(broadcastTx, fundingOutput, scriptSigIndex);
229+
230+
//assert that funding transaction is spending correctly
231+
Set<Script.VerifyFlag> verifyFlags = new HashSet<Script.VerifyFlag>(Arrays.asList(Script.VerifyFlag.SIGHASH_FORKID));
232+
interp.correctlySpends(signedTx.getInputs().get(scriptSigIndex).getScriptSig(), fundingOutput.getScript(), broadcastTx, scriptSigIndex, verifyFlags, Coin.valueOf(fundingValue));
233+
}
155234
}).doesNotThrowAnyException();
156235
}
157236

@@ -165,6 +244,7 @@ public void builderCanSpendFromUtxoMap() throws InvalidKeyException, IOException
165244
String wif = "cRTUuWgPdp7tJPrn1Xeq196eZa4ZCpg8n3cgDJsJmgDHBZ8x9fpv";
166245
PrivateKey privateKey = PrivateKey.fromWIF(wif);
167246

247+
System.out.println(Address.fromKey(NetworkAddressType.TEST_PKH, privateKey.getPublicKey()).toString());
168248
JsonNode json = new ObjectMapper().readTree(
169249
new InputStreamReader(getClass().getResourceAsStream("multi_input.json"),
170250
StandardCharsets.UTF_8)
@@ -202,6 +282,30 @@ public void builderCanSpendFromUtxoMap() throws InvalidKeyException, IOException
202282
.spendTo(new P2PKHLockBuilder(recipientAddress), BigInteger.valueOf(100000))
203283
.sendChangeTo(recipientAddress)
204284
.build(true);
285+
286+
//new Script Interpreter to help us verify our spending conditions
287+
Interpreter interp = new Interpreter();
288+
289+
//loop over every one of our spending inputs and verify we are
290+
//correctly spending those outputs
291+
for (int scriptSigIndex = 0; scriptSigIndex < 0; scriptSigIndex++) {
292+
TransactionInput spendingInputOne = TransactionInput.fromByteArray(broadcastTx.getInputs().get(0).serialize());
293+
String fundingTxId = Utils.HEX.encode(spendingInputOne.getPrevTxnId());
294+
Integer fundingOutputIndex = json.get(scriptSigIndex).get("tx_pos").asInt();
295+
Long fundingValue = json.get(scriptSigIndex).get("value").asLong();
296+
297+
//lookup funding transaction corresponding to first output
298+
String rawFundingTx = json.get(scriptSigIndex).get("raw_tx").asText();
299+
Transaction fundingTxOne = Transaction.fromHex(rawFundingTx);
300+
TransactionOutput fundingOutput = fundingTxOne.getOutputs().get(fundingOutputIndex);
301+
302+
TransactionSigner signer = new TransactionSigner(SigHashType.FORKID.value | SigHashType.ALL.value, privateKey);
303+
Transaction signedTx = signer.sign(broadcastTx, fundingOutput, scriptSigIndex);
304+
305+
//assert that funding transaction is spending correctly
306+
Set<Script.VerifyFlag> verifyFlags = new HashSet<Script.VerifyFlag>(Arrays.asList(Script.VerifyFlag.SIGHASH_FORKID));
307+
interp.correctlySpends(signedTx.getInputs().get(scriptSigIndex).getScriptSig(), fundingOutput.getScript(), broadcastTx, scriptSigIndex, verifyFlags, Coin.valueOf(fundingValue));
308+
}
205309
}).doesNotThrowAnyException();
206310
}
207311
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
[
2+
{
3+
"height": 1433834,
4+
"value": 500000000,
5+
"tx_pos": 0,
6+
"raw_tx": "0200000001f4145c67f9678e55584c9e258b3de751f74d1ceebd0cdfa7391fef8945e650a300000000484730440220717b3cc7930b5b7114e92dc239c27aa69314898aae642be43a731b696cddd0100220043d63d6b0f7b16cca5bcaed722ad80ec36480c112c7c6c34e4fe2b4f0837ba841feffffff020065cd1d000000001976a914191f99293aedd9f66e090e296abe2c64cabdbc5788ac80587307000000001976a914cad75dd4a8ea76c18ad5e561afd013daa4a8d0ec88ac6a020000",
7+
"tx_hash": "9dc51e14a93ef538affe675200aeec15aedc0378c093074cf302f4d731eb484f"
8+
},
9+
{
10+
"height": 1433834,
11+
"value": 700000000,
12+
"tx_pos": 0,
13+
"tx_hash": "d2cbb85329f1f14f94e028edb8d2bc21cb3ffe9aed8b58f8741571580a186a38",
14+
"raw_tx": "0200000001ddbb28b78fc3f6501afff0bbff1add8aea9301d906a706362b9ac9650418f6050000000047463043021f1f31214b5fa7deb37a72bfb4de421048e338669eec554d5dd7edbe3af337e302200e58d8a7964d0d26319950fa3ed3599dc9e338fea83577c207e9f788c77b604341feffffff020027b929000000001976a914191f99293aedd9f66e090e296abe2c64cabdbc5788acc054c820000000001976a91468bda6c59f5211caaefd49d69e45c56d88b4cd3788ac6a020000"
15+
},
16+
{
17+
"height": 1433834,
18+
"value": 600000000,
19+
"tx_pos": 1,
20+
"raw_tx": "02000000011a44b1703870432cb26a89a269b1bd546b3ff5fdc7775d5dda07154c897c878a000000004847304402205ebe801f4a5a693a607eac50d217653271295df3421b3c2c502739da04ecd2e1022019c018c1cd8f8692872b8f7e8ab61bc912b32e22f57ab680ccd7d4d8a4c97ad941feffffff0280777d01000000001976a914a99d5811b500ba6dec61dc4bf95fa96f4d610e6688ac0046c323000000001976a914191f99293aedd9f66e090e296abe2c64cabdbc5788ac6a020000",
21+
"tx_hash": "67e7555620688f285b76f7cb4698679ffbd006029bd9c51d36d153b8bdbeb377"
22+
}
23+
]

0 commit comments

Comments
 (0)