forked from mycelium-com/wallet-android
/
StandardTransactionBuilder.java
462 lines (400 loc) · 18 KB
/
StandardTransactionBuilder.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
/*
* Copyright 2013 Megion Research & Development GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.mrd.bitlib;
import java.io.Serializable;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Function;
import com.google.common.base.Preconditions;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
import com.mrd.bitlib.crypto.BitcoinSigner;
import com.mrd.bitlib.crypto.PrivateKeyRing;
import com.mrd.bitlib.crypto.PublicKey;
import com.mrd.bitlib.crypto.PublicKeyRing;
import com.mrd.bitlib.crypto.RandomSource;
import com.mrd.bitlib.model.*;
import com.mrd.bitlib.util.ByteWriter;
import com.mrd.bitlib.util.CoinUtil;
import com.mrd.bitlib.util.HashUtils;
import com.mrd.bitlib.util.Sha256Hash;
public class StandardTransactionBuilder {
public static final long MINIMUM_MINER_FEE = 1000000; //Changed to min fee of Peercoin
//public static final long MINIMUM_OUTPUT_VALUE = 5430; //original
public static final long MINIMUM_OUTPUT_VALUE = MINIMUM_MINER_FEE; //Since the minimum miner fee is the minimum output value for peercoin
public static class InsufficientFundsException extends Exception {
//todo consider refactoring this into a composite return value instead of an exception. it is not really "exceptional"
private static final long serialVersionUID = 1L;
public long sending;
public long fee;
public InsufficientFundsException(long sending, long fee) {
super("Insufficient funds to send " + sending + " peers with fee " + fee);
this.sending = sending;
this.fee = fee;
}
}
public static class OutputTooSmallException extends Exception {
//todo consider refactoring this into a composite return value instead of an exception. it is not really "exceptional"
private static final long serialVersionUID = 1L;
public long value;
public OutputTooSmallException(long value) {
super("An output was added with a value of " + value
+ " peers, which is smaller than the minimum accepted by the Peercoin network");
}
}
public static class SigningRequest implements Serializable {
private static final long serialVersionUID = 1L;
// The public part of the key we will sign with
public PublicKey publicKey;
// The data to make a signature on. For transactions this is the
// transaction hash
public Sha256Hash toSign;
public SigningRequest(PublicKey publicKey, Sha256Hash toSign) {
this.publicKey = publicKey;
this.toSign = toSign;
}
}
public static class UnsignedTransaction implements Serializable {
private static final long serialVersionUID = 1L;
private TransactionOutput[] _outputs;
private UnspentTransactionOutput[] _funding;
private SigningRequest[] _signingRequests;
private NetworkParameters _network;
private UnsignedTransaction(List<TransactionOutput> outputs, List<UnspentTransactionOutput> funding,
PublicKeyRing keyRing, NetworkParameters network) {
_network = network;
_outputs = outputs.toArray(new TransactionOutput[]{});
_funding = funding.toArray(new UnspentTransactionOutput[]{});
_signingRequests = new SigningRequest[_funding.length];
// Create empty input scripts pointing at the right out points
TransactionInput[] inputs = new TransactionInput[_funding.length];
for (int i = 0; i < _funding.length; i++) {
inputs[i] = new TransactionInput(_funding[i].outPoint, ScriptInput.EMPTY);
}
// Create transaction with valid outputs and empty inputs
Transaction transaction = new Transaction(1, inputs, _outputs, 0);
for (int i = 0; i < _funding.length; i++) {
UnspentTransactionOutput f = _funding[i];
// Make sure that we only work on standard output scripts
if (!(f.script instanceof ScriptOutputStandard)) {
throw new RuntimeException("Unsupported script");
}
// Find the address of the funding
byte[] addressBytes = ((ScriptOutputStandard) f.script).getAddressBytes();
Address address = Address.fromStandardBytes(addressBytes, _network);
// Find the key to sign with
PublicKey publicKey = keyRing.findPublicKeyByAddress(address);
if (publicKey == null) {
// This should not happen as we only work on outputs that we have
// keys for
throw new RuntimeException("Public key not found");
}
// Set the input script to the funding output script
inputs[i].script = ScriptInput.fromOutputScript(_funding[i].script);
// Calculate the transaction hash that has to be signed
Sha256Hash hash = hashTransaction(transaction);
// Set the input to the empty script again
inputs[i] = new TransactionInput(_funding[i].outPoint, ScriptInput.EMPTY);
_signingRequests[i] = new SigningRequest(publicKey, hash);
}
}
public SigningRequest[] getSignatureInfo() {
return _signingRequests;
}
public long calculateFee() {
long in = 0, out = 0;
for (UnspentTransactionOutput funding : _funding) {
in += funding.value;
}
for (TransactionOutput output : _outputs) {
out += output.value;
}
return in - out;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
String fee = CoinUtil.valueString(calculateFee(), false);
sb.append(String.format("Fee: %s", fee)).append('\n');
int max = Math.max(_funding.length, _outputs.length);
for (int i = 0; i < max; i++) {
UnspentTransactionOutput in = i < _funding.length ? _funding[i] : null;
TransactionOutput out = i < _outputs.length ? _outputs[i] : null;
String line;
if (in != null && out != null) {
line = String.format("%36s %13s -> %36s %13s", getAddress(in.script, _network), getValue(in.value),
getAddress(out.script, _network), getValue(out.value));
} else if (in != null && out == null) {
line = String.format("%36s %13s %36s %13s", getAddress(in.script, _network), getValue(in.value), "",
"");
} else if (in == null && out != null) {
line = String.format("%36s %13s %36s %13s", "", "", getAddress(out.script, _network),
getValue(out.value));
} else {
line = "";
}
sb.append(line).append('\n');
}
return sb.toString();
}
private String getAddress(ScriptOutput script, NetworkParameters network) {
Address address = script.getAddress(network);
if (address == null) {
return "Unknown";
}
return address.toString();
}
private String getValue(Long value) {
return String.format("(%s)", CoinUtil.valueString(value, false));
}
}
private NetworkParameters _network;
private List<TransactionOutput> _outputs;
public StandardTransactionBuilder(NetworkParameters network) {
_network = network;
_outputs = new LinkedList<TransactionOutput>();
}
public void addOutput(Address sendTo, long value) throws OutputTooSmallException {
if (value < MINIMUM_OUTPUT_VALUE) {
throw new OutputTooSmallException(value);
}
_outputs.add(createOutput(sendTo, value));
}
private TransactionOutput createOutput(Address sendTo, long value) {
ScriptOutput script;
if (sendTo.isMultisig(_network)) {
script = new ScriptOutputMultisig(sendTo.getTypeSpecificBytes());
} else {
script = new ScriptOutputStandard(sendTo.getTypeSpecificBytes());
}
TransactionOutput output = new TransactionOutput(value, script);
return output;
}
public static List<byte[]> generateSignatures(SigningRequest[] requests, PrivateKeyRing keyRing,
RandomSource randomSource) {
List<byte[]> signatures = new LinkedList<byte[]>();
for (SigningRequest request : requests) {
BitcoinSigner signer = keyRing.findSignerByPublicKey(request.publicKey);
if (signer == null) {
// This should not happen as we only work on outputs that we have
// keys for
throw new RuntimeException("Private key not found");
}
byte[] signature = signer.makeStandardBitcoinSignature(request.toSign, randomSource);
signatures.add(signature);
}
return signatures;
}
/**
* Create an unsigned transaction without specifying a fee. The fee is
* automatically calculated to pass minimum relay and mining requirements.
*
* @param unspent The list of unspent transaction outputs that can be used as
* funding
* @param changeAddress The address to send any change to. If null the change address
* will be set to the address of one of the spent outputs.
* @param keyRing The public key ring matching the unspent outputs
* @param network The network we are working on
* @return An unsigned transaction or null if not enough funds were available
* @throws InsufficientFundsException
*/
public UnsignedTransaction createUnsignedTransaction(List<UnspentTransactionOutput> unspent, Address changeAddress,
PublicKeyRing keyRing, NetworkParameters network) throws InsufficientFundsException {
long fee = MINIMUM_MINER_FEE;
while (true) {
UnsignedTransaction unsigned;
try {
unsigned = createUnsignedTransaction(unspent, changeAddress, fee, keyRing, network);
} catch (InsufficientFundsException e) {
// We did not even have enough funds to pay the minimum fee
throw e;
}
int txSize = estimateTransacrionSize(unsigned);
// fee is based on the size of the transaction, we have to pay for
// every 1000 bytes
long requiredFee = (1 + (txSize / 1000)) * MINIMUM_MINER_FEE;
if (fee >= requiredFee) {
return unsigned;
}
// collect coins anew with an increased fee
fee += MINIMUM_MINER_FEE;
}
}
/**
* Create an unsigned transaction with a specific miner fee. Note that
* specifying a miner fee that is too low may result in hanging transactions
* that never confirm.
*
* @param inventory The list of unspent transaction outputs that can be used as
* funding
* @param changeAddress The address to send any change to
* @param fee The miner fee to pay. Specifying zero may result in hanging
* transactions.
* @param keyRing The public key ring matching the unspent outputs
* @param network The network we are working on
* @return An unsigned transaction or null if not enough funds were available
* @throws InsufficientFundsException
*/
public UnsignedTransaction createUnsignedTransaction(List<UnspentTransactionOutput> inventory,
Address changeAddress, long fee, PublicKeyRing keyRing, NetworkParameters network)
throws InsufficientFundsException {
// Make a copy so we can mutate the list
List<UnspentTransactionOutput> unspent = new LinkedList<UnspentTransactionOutput>(inventory);
List<UnspentTransactionOutput> funding = new LinkedList<UnspentTransactionOutput>();
long outputSum = outputSum();
long toSend = fee + outputSum;
long found = 0;
while (found < toSend) {
UnspentTransactionOutput output = extractOldest(unspent);
if (output == null) {
// We do not have enough funds
throw new InsufficientFundsException(outputSum, fee);
}
found += output.value;
funding.add(output);
}
if (changeAddress == null) {
// If no change address s specified, get the richest address from the
// funding set
changeAddress = extractRichest(funding, network);
}
// We have our funding, calculate change
long change = found - toSend;
// Get a copy of all outputs
List<TransactionOutput> outputs = new LinkedList<TransactionOutput>(_outputs);
if (change > 0) {
// We have more funds than needed, add an output to our change address
if (change >= MINIMUM_OUTPUT_VALUE) {
// But only if the change is larger than the minimum output accepted
// by the network
outputs.add(createOutput(changeAddress, change));
} else {
// The change output would be smaller than what the network would
// accept. In this case we leave it be as a small increased miner
// fee.
}
}
return new UnsignedTransaction(outputs, funding, keyRing, network);
}
@VisibleForTesting
Address extractRichest(Collection<UnspentTransactionOutput> unspent, final NetworkParameters network) {
Preconditions.checkArgument(!unspent.isEmpty());
Function<UnspentTransactionOutput, Address> txout2Address = new Function<UnspentTransactionOutput, Address>() {
@Override
public Address apply(UnspentTransactionOutput input) {
return input.script.getAddress(network);
}
};
Multimap<Address, UnspentTransactionOutput> index = Multimaps.index(unspent, txout2Address);
Address ret = extractRichest(index);
return Preconditions.checkNotNull(ret);
}
private Address extractRichest(Multimap<Address, UnspentTransactionOutput> index) {
Address ret = null;
long maxSum = 0;
for (Address address : index.keys()) {
Collection<UnspentTransactionOutput> unspentTransactionOutputs = index.get(address);
long newSum = sum(unspentTransactionOutputs);
if (newSum > maxSum)
ret = address;
maxSum = newSum;
}
return ret;
}
private long sum(Iterable<UnspentTransactionOutput> outputs) {
long sum = 0;
for (UnspentTransactionOutput output : outputs) {
sum += output.value;
}
return sum;
}
public static Transaction finalizeTransaction(UnsignedTransaction unsigned, List<byte[]> signatures) {
// Create finalized transaction inputs
TransactionInput[] inputs = new TransactionInput[unsigned._funding.length];
for (int i = 0; i < unsigned._funding.length; i++) {
// Create script from signature and public key
ScriptInputStandard script = new ScriptInputStandard(signatures.get(i),
unsigned._signingRequests[i].publicKey.getPublicKeyBytes());
inputs[i] = new TransactionInput(unsigned._funding[i].outPoint, script);
}
// Create transaction with valid outputs and empty inputs
Transaction transaction = new Transaction(1, inputs, unsigned._outputs, 0);
return transaction;
}
private UnspentTransactionOutput extractOldest(Collection<UnspentTransactionOutput> unspent) {
// find the "oldest" output
int minHeight = Integer.MAX_VALUE;
UnspentTransactionOutput oldest = null;
for (UnspentTransactionOutput output : unspent) {
if (!(output.script instanceof ScriptOutputStandard)) {
// only look for standard scripts
continue;
}
if (output.height < minHeight) {
minHeight = output.height;
oldest = output;
}
}
if (oldest == null) {
// There were no outputs
return null;
}
unspent.remove(oldest);
return oldest;
}
private long outputSum() {
long sum = 0;
for (TransactionOutput output : _outputs) {
sum += output.value;
}
return sum;
}
private static Sha256Hash hashTransaction(Transaction t) {
ByteWriter writer = new ByteWriter(1024);
t.toByteWriter(writer);
// We also have to write a hash type.
int hashType = 1;
writer.putIntLE(hashType);
// Note that this is NOT reversed to ensure it will be signed
// correctly. If it were to be printed out
// however then we would expect that it is IS reversed.
return HashUtils.doubleSha256(writer.toBytes());
}
/**
* Estimate transaction size by clearing all input scripts and adding 140
* bytes for each input. (The type of scripts we generate are 138-140 bytes
* long). This allows us to give a good estimate of the final transaction
* size, and determine whether out fee size is large enough.
*
* @param unsigned The unsigned transaction to estimate the size of
* @return The estimated transaction size
*/
private static int estimateTransacrionSize(UnsignedTransaction unsigned) {
// Create fake empty inputs
TransactionInput[] inputs = new TransactionInput[unsigned._funding.length];
for (int i = 0; i < inputs.length; i++) {
inputs[i] = new TransactionInput(unsigned._funding[i].outPoint, ScriptInput.EMPTY);
}
// Create fake transaction
Transaction t = new Transaction(1, inputs, unsigned._outputs, 0);
int txSize = t.toBytes().length;
// Add maximum size for each input
txSize += 140 * t.inputs.length;
return txSize;
}
}