Skip to content

Commit be70d9b

Browse files
authored
Merge pull request #16 from twostack/txbuilder-fees
TransactionBuilder Signature generation refactor
2 parents 44b33e1 + d8fb7e6 commit be70d9b

File tree

8 files changed

+293
-15
lines changed

8 files changed

+293
-15
lines changed

CHANGELOG.md

+15
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,18 @@
1+
#Release 1.6.0
2+
### TransactionBuilder Signature generation refactor
3+
4+
Transaction building suffered from a rather pernicious problem
5+
wherein it becomes hard/complicated to calculate fees.
6+
This stems from the fact that when you try to large number of
7+
utxos, the consequent large number of inputs in the spending tx
8+
leads to guesswork about the appropriate fee calculation.
9+
10+
This update refactors the process of Transaction Signing so that
11+
the builder can directly generate the signed inputs and therefore
12+
perform the work of fee calculation internally.
13+
14+
Please see the transaction/TransactionBuilderTest.java for an example use.
15+
116
# Release 1.5.5
217
Made constructors public so to allow outside-package subclassing
318

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.5.5'
9+
version '1.6.0'
1010

1111
repositories {
1212
mavenCentral()

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

+83-1
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,16 @@
1818

1919
import org.twostack.bitcoin4j.Address;
2020
import org.twostack.bitcoin4j.Utils;
21+
import org.twostack.bitcoin4j.exception.SigHashException;
22+
import org.twostack.bitcoin4j.exception.SignatureDecodeException;
2123
import org.twostack.bitcoin4j.exception.TransactionException;
2224
import org.twostack.bitcoin4j.script.Script;
2325

2426
import javax.annotation.Nullable;
27+
import java.io.IOException;
2528
import java.math.BigInteger;
2629
import java.util.*;
30+
import java.util.stream.Stream;
2731

2832
import static org.twostack.bitcoin4j.Utils.HEX;
2933

@@ -63,6 +67,29 @@ public class TransactionBuilder {
6367

6468
private long nLockTime = 0;
6569

70+
private HashMap<String, SignerDto> signerMap = new HashMap();
71+
72+
73+
private class SignerDto{
74+
private TransactionSigner signer;
75+
private TransactionOutpoint outpoint;
76+
77+
private SignerDto(){}
78+
79+
SignerDto(TransactionSigner signer, TransactionOutpoint outpoint){
80+
this.signer = signer;
81+
this.outpoint = outpoint;
82+
}
83+
84+
public TransactionSigner getSigner() {
85+
return signer;
86+
}
87+
88+
public TransactionOutpoint getOutpoint() {
89+
return outpoint;
90+
}
91+
}
92+
6693
/*
6794
utxoMap is expected to have :
6895
@@ -99,6 +126,36 @@ public TransactionBuilder spendFromUtxoMap(Map<String, Object> utxoMap, @Nullabl
99126

100127
}
101128

129+
public TransactionBuilder spendFromTransaction(TransactionSigner signer, Transaction txn, int outputIndex, long sequenceNumber, UnlockingScriptBuilder unlocker){
130+
131+
//save the transactionId. This is expensive operation which serialises the Tx.
132+
String transactionId = txn.getTransactionId();
133+
134+
//construct the data to save to signerMap
135+
TransactionOutput output = txn.getOutputs().get(outputIndex);
136+
137+
TransactionOutpoint outpoint = new TransactionOutpoint();
138+
outpoint.setOutputIndex(outputIndex);
139+
outpoint.setLockingScript(output.getScript());
140+
outpoint.setSatoshis(output.getAmount());
141+
outpoint.setTransactionId(transactionId);
142+
143+
this.signerMap.put(transactionId, new SignerDto(signer, outpoint));
144+
145+
//update the spending transactionInput
146+
TransactionInput input = new TransactionInput(
147+
Utils.reverseBytes(txn.getTransactionIdBytes()),
148+
outputIndex,
149+
sequenceNumber,
150+
unlocker
151+
);
152+
153+
spendingMap.put(transactionId, txn.getOutputs().get(outputIndex).getAmount());
154+
155+
inputs.add(input);
156+
return this;
157+
}
158+
102159
public TransactionBuilder spendFromTransaction(Transaction txn, int outputIndex, long sequenceNumber, UnlockingScriptBuilder unlocker){
103160

104161
TransactionInput input = new TransactionInput(
@@ -264,7 +321,8 @@ TransactionBuilder lockUntilBlockHeight(int blockHeight) {
264321
}
265322
*/
266323

267-
public Transaction build(boolean performChecks) throws TransactionException {
324+
325+
public Transaction build(boolean performChecks) throws TransactionException, IOException, SigHashException, SignatureDecodeException {
268326
if (performChecks){
269327
runTransactionChecks();
270328
}
@@ -277,6 +335,30 @@ public Transaction build(boolean performChecks) throws TransactionException {
277335
//add transaction outputs
278336
tx.addOutputs(outputs);
279337

338+
//update inputs with signatures
339+
String txId = tx.getTransactionId();
340+
for (int index = 0; index < inputs.size() ; index++) {
341+
TransactionInput currentInput = inputs.get(index);
342+
343+
Optional<Map.Entry<String, SignerDto>> result = signerMap.entrySet().stream().filter( (Map.Entry<String, SignerDto> entry) -> {
344+
//drop everything from stream except what we're looking for
345+
return !(entry.getValue().outpoint.getTransactionId() == HEX.encode(currentInput.getPrevTxnId()) &&
346+
entry.getValue().outpoint.getOutputIndex() == currentInput.getPrevTxnOutputIndex());
347+
}).findFirst();
348+
349+
if (result.isPresent()) {
350+
351+
SignerDto dto = result.get().getValue();
352+
TransactionOutput utxoToSpend = new TransactionOutput(dto.outpoint.getSatoshis(), dto.outpoint.getLockingScript());
353+
354+
//TODO: this side-effect programming where the signer mutates my local variable
355+
// still bothers me.
356+
dto.signer.sign(tx, utxoToSpend, index);
357+
}
358+
}
359+
360+
361+
280362
if (changeScriptBuilder != null) {
281363
tx.addOutput(getChangeOutput());
282364
}

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

+56
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ public class TransactionSigner {
3434
private Transaction signedTransaction;
3535
private int sigHashType;
3636
private TransactionSignature signature;
37+
private PrivateKey signingKey;
38+
3739

3840
public byte[] getHash() {
3941
return hash;
@@ -55,6 +57,52 @@ public byte[] getPreImage() {
5557
return preImage;
5658
}
5759

60+
/** Constructs a new instance of the TransactionSigner.
61+
* NOTE: The SigHashType for signing will default to a value of
62+
* (SigHashType.ALL.value | SigHashType.FORKID.value)
63+
*/
64+
// TransactionSigner(){
65+
// this.sigHashType = SigHashType.ALL.value | SigHashType.FORKID.value;
66+
// }
67+
68+
/** Constructs a new instance of the TransactionSigner.
69+
* NOTE: The SigHashType for signing will default to a value of
70+
* (SigHashType.ALL.value | SigHashType.FORKID.value)
71+
*
72+
* @param sigHashType - Flags that govern which SigHash algorithm to use during signature generation
73+
*/
74+
TransactionSigner(int sigHashType, PrivateKey signingKey){
75+
this.sigHashType = sigHashType;
76+
this.signingKey = signingKey;
77+
}
78+
79+
/** Signs the provided transaction, and populates the corresponding input's
80+
* LockingScriptBuilder with the signature. Responsibility for what to
81+
* do with the Signature (populate appropriate template) is left to the
82+
* LockingScriptBuilder instance.
83+
*
84+
* NOTE: This invocation will use the SigHashType set as part of the
85+
* constructor invocation. If the default constructor is invoked then
86+
* the SigHashType defaults to (SigHashType.ALL.value | SigHashType.FORKID.value) :
87+
*
88+
*
89+
* @param unsignedTxn - Unsigned Transaction
90+
* @param utxo - Funding transaction's Output to sign over
91+
* @param inputIndex - Input of the current Transaction we are signing for
92+
* @return Signed Transaction
93+
* @throws TransactionException
94+
* @throws IOException
95+
* @throws SigHashException
96+
* @throws SignatureDecodeException
97+
*/
98+
public Transaction sign(
99+
Transaction unsignedTxn,
100+
TransactionOutput utxo,
101+
int inputIndex) throws TransactionException, IOException, SigHashException, SignatureDecodeException {
102+
103+
return this.sign(unsignedTxn, utxo, inputIndex, signingKey, sigHashType);
104+
}
105+
58106
/** Signs the provided transaction, and populates the corresponding input's
59107
* LockingScriptBuilder with the signature. Responsibility for what to
60108
* do with the Signature (populate appropriate template) is left to the
@@ -120,6 +168,14 @@ public TransactionSignature signPreimage(PrivateKey signingKey, byte[] preImage,
120168
return new TransactionSignature(ecSig.r, ecSig.s, sigHashType);
121169
}
122170

171+
public PrivateKey getSigningKey() {
172+
return signingKey;
173+
}
174+
175+
public void setSigningKey (PrivateKey privateKey) {
176+
this.signingKey = privateKey;
177+
}
178+
123179

124180
/** sf:> This seems like more Core buggery to me. What sort of TX remains valid without proper SighashType ???
125181
*

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

+7-5
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ public void canCreateP2PKHScript(){
159159
}
160160

161161
@Test
162-
public void canUseP2PKInTransactionBuilder() throws InvalidKeyException, TransactionException {
162+
public void canUseP2PKInTransactionBuilder() throws InvalidKeyException, TransactionException, SigHashException, SignatureDecodeException, IOException {
163163

164164
//Create a Transaction instance from the RAW transaction data create by bitcoin-cli.
165165
//this transaction contains the UTXO we are interested in
@@ -181,7 +181,8 @@ public void canUseP2PKInTransactionBuilder() throws InvalidKeyException, Transac
181181

182182
//simply check that we have clean e2e execution
183183
Assertions.assertThatCode(() -> {
184-
new TransactionSigner().sign(unsignedTxn, utxoToSign,0, privateKey, SigHashType.ALL.value | SigHashType.FORKID.value);
184+
TransactionSigner signer = new TransactionSigner( SigHashType.ALL.value | SigHashType.FORKID.value, privateKey);
185+
signer.sign(unsignedTxn, utxoToSign,0);
185186
}).doesNotThrowAnyException();
186187
}
187188

@@ -323,10 +324,11 @@ public void canSpendFromMultiSig() throws InvalidKeyException, TransactionExcept
323324
.withFeePerKb(512)
324325
.build(false);
325326

326-
TransactionSigner signer = new TransactionSigner();
327+
TransactionSigner signer1 = new TransactionSigner(SigHashType.ALL.value | SigHashType.FORKID.value, private1 );
328+
TransactionSigner signer2 = new TransactionSigner(SigHashType.ALL.value | SigHashType.FORKID.value, private2 );
327329
TransactionOutput utxo = new TransactionOutput(BigInteger.valueOf(100000), lockBuilder);
328-
signer.sign(tx, utxo, 0, private1, SigHashType.ALL.value | SigHashType.FORKID.value);
329-
signer.sign(tx, utxo, 0, private2, SigHashType.ALL.value | SigHashType.FORKID.value);
330+
signer1.sign(tx, utxo, 0);
331+
signer2.sign(tx, utxo, 0);
330332

331333
assertEquals(2, unlockBuilder.getSignatures().size() );
332334

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package org.twostack.bitcoin4j.transaction;
2+
3+
import com.fasterxml.jackson.databind.JsonNode;
4+
import com.fasterxml.jackson.databind.ObjectMapper;
5+
import org.assertj.core.api.Assertions;
6+
import org.junit.Test;
7+
import org.twostack.bitcoin4j.Address;
8+
import org.twostack.bitcoin4j.PrivateKey;
9+
import org.twostack.bitcoin4j.Utils;
10+
import org.twostack.bitcoin4j.exception.InvalidKeyException;
11+
import org.twostack.bitcoin4j.exception.SigHashException;
12+
import org.twostack.bitcoin4j.exception.SignatureDecodeException;
13+
import org.twostack.bitcoin4j.exception.TransactionException;
14+
import org.twostack.bitcoin4j.params.NetworkAddressType;
15+
import org.twostack.bitcoin4j.script.Script;
16+
import org.twostack.bitcoin4j.script.ScriptError;
17+
18+
import java.io.IOException;
19+
import java.io.InputStreamReader;
20+
import java.math.BigInteger;
21+
import java.nio.charset.StandardCharsets;
22+
import java.util.Set;
23+
24+
import static org.twostack.bitcoin4j.utils.TestUtil.parseVerifyFlags;
25+
26+
public class TransactionBuilderTest {
27+
28+
@Test
29+
public void processAndSignMultiInput() throws IOException, InvalidKeyException, SignatureDecodeException, TransactionException, SigHashException {
30+
31+
//This WIF is for a private key that actually has testnet coins on TESTNET
32+
//The transactions in multi_input.json are UTXOs that exist(ed) on TESTNET
33+
// at time of writing this test, and can be viewed on TESTNET using a block explorer
34+
String wif = "cRTUuWgPdp7tJPrn1Xeq196eZa4ZCpg8n3cgDJsJmgDHBZ8x9fpv";
35+
PrivateKey privateKey = PrivateKey.fromWIF(wif);
36+
37+
JsonNode json = new ObjectMapper().readTree(
38+
new InputStreamReader(getClass().getResourceAsStream("multi_input.json"),
39+
StandardCharsets.UTF_8)
40+
);
41+
42+
//build one large transaction that spends all the inputs
43+
TransactionBuilder builder = new TransactionBuilder();
44+
for (JsonNode utxoInfo : json) {
45+
46+
Integer fundingOutputIndex = utxoInfo.get("tx_pos").asInt();
47+
String rawTxHex = utxoInfo.get("raw_tx").asText();
48+
49+
Transaction fundingTx = Transaction.fromHex(rawTxHex);
50+
51+
UnlockingScriptBuilder unlocker = new P2PKHUnlockBuilder(privateKey.getPublicKey());
52+
53+
TransactionSigner signer = new TransactionSigner(SigHashType.ALL.value | SigHashType.FORKID.value, privateKey);
54+
builder.spendFromTransaction(signer, fundingTx, fundingOutputIndex, TransactionInput.MAX_SEQ_NUMBER, unlocker);
55+
56+
}
57+
58+
Address recipientAddress = Address.fromKey(NetworkAddressType.TEST_PKH, privateKey.getPublicKey());
59+
60+
61+
Assertions.assertThatCode(() -> {
62+
Transaction broadcastTx = builder.withFeePerKb(512)
63+
.spendTo(new P2PKHLockBuilder(recipientAddress), BigInteger.valueOf(100000))
64+
.sendChangeTo(recipientAddress)
65+
.build(true);
66+
}).doesNotThrowAnyException();
67+
68+
}
69+
70+
}

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

+8-8
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ public void TestTransactionIdMatchesTransactionHash() {
7878
}
7979

8080
@Test
81-
public void canCreateAndSignTransactionWithoutChange() throws InvalidKeyException, TransactionException, IOException, SigHashException {
81+
public void canCreateAndSignTransactionWithoutChange() throws InvalidKeyException, TransactionException, IOException, SigHashException, SignatureDecodeException {
8282

8383
PrivateKey privateKey = PrivateKey.fromWIF("cVVvUsNHhbrgd7aW3gnuGo2qJM45LhHhTCVXrDSJDDcNGE6qmyCs");
8484
Address changeAddress = Address.fromString(NetworkType.TEST, "mu4DpTaD75nheE4z5CQazqm1ivej1vzL4L"); // my address
@@ -103,14 +103,15 @@ public void canCreateAndSignTransactionWithoutChange() throws InvalidKeyExceptio
103103

104104
//simply check that we have clean e2e execution
105105
Assertions.assertThatCode(() -> {
106-
new TransactionSigner().sign(unsignedTxn, utxoToSign,0, privateKey, SigHashType.ALL.value | SigHashType.FORKID.value);
106+
TransactionSigner signer = new TransactionSigner(SigHashType.ALL.value | SigHashType.FORKID.value, privateKey);
107+
signer.sign(unsignedTxn, utxoToSign,0);
107108
}).doesNotThrowAnyException();
108109

109110
//System.out.println(HEX.encode(signedTx.serialize()));
110111
}
111112

112113
@Test
113-
public void can_create_and_sign_transaction() throws InvalidKeyException, TransactionException, IOException, SigHashException {
114+
public void can_create_and_sign_transaction() throws InvalidKeyException, TransactionException, IOException, SigHashException, SignatureDecodeException {
114115

115116
PrivateKey privateKey = PrivateKey.fromWIF("cVVvUsNHhbrgd7aW3gnuGo2qJM45LhHhTCVXrDSJDDcNGE6qmyCs");
116117
Address changeAddress = Address.fromString(NetworkType.TEST, "mu4DpTaD75nheE4z5CQazqm1ivej1vzL4L"); // my address
@@ -136,7 +137,8 @@ public void can_create_and_sign_transaction() throws InvalidKeyException, Transa
136137

137138
//simply check that we have clean e2e execution
138139
Assertions.assertThatCode(() -> {
139-
new TransactionSigner().sign(unsignedTxn, utxoToSign,0, privateKey, SigHashType.ALL.value | SigHashType.FORKID.value);
140+
TransactionSigner signer = new TransactionSigner(SigHashType.ALL.value | SigHashType.FORKID.value, privateKey );
141+
signer.sign(unsignedTxn, utxoToSign,0);
140142
}).doesNotThrowAnyException();
141143

142144
//System.out.println(HEX.encode(signedTx.serialize()));
@@ -204,13 +206,11 @@ public void test_transaction_serialization_vectors() throws IOException, Transac
204206

205207
Transaction tx = builder.build(false);
206208

207-
TransactionSigner signer = new TransactionSigner();
209+
TransactionSigner signer = new TransactionSigner( sighashType, privateKey);
208210
signer.sign(
209211
tx,
210212
new TransactionOutput(BigInteger.valueOf(satoshis), scriptPubKey),
211-
0,
212-
privateKey,
213-
sighashType);
213+
0 );
214214

215215
assertEquals(serializedTx, HEX.encode(tx.serialize()));
216216

0 commit comments

Comments
 (0)