Skip to content

Commit 3d18770

Browse files
committed
Add Locktime abstraction
1 parent 2d5de6e commit 3d18770

File tree

11 files changed

+236
-24
lines changed

11 files changed

+236
-24
lines changed

coinlib/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ secp256k1-coinlib fork.
1010
`0xfffffffe` by default and enforce locktimes.
1111
- Adds `locktimeIsEnforced` to `Transaction` to determine if the locktime is in
1212
effect.
13+
- Adds `Locktime` abstractions for transactions. Transactions have
14+
`.isUnlocked` to determine if the transaction is available for block
15+
inclusion.
1316
- Moves to underlying secp256k1-coinlib.
1417
- Removed dependency to wasm_interop that had a broken js dependency.
1518
- Fixes `extraEntropy` being ignored for `Secp256k1Base.schnorrSign`.

coinlib/lib/src/coinlib_base.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export 'package:coinlib/src/taproot/taproot.dart';
4242
export 'package:coinlib/src/tx/coin_selection.dart';
4343
export 'package:coinlib/src/tx/transaction.dart';
4444
export 'package:coinlib/src/tx/sign_details.dart';
45+
export 'package:coinlib/src/tx/locktime.dart';
4546
export 'package:coinlib/src/tx/outpoint.dart';
4647
export 'package:coinlib/src/tx/output.dart';
4748

coinlib/lib/src/tx/coin_selection.dart

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import 'package:coinlib/src/tx/inputs/witness_input.dart';
55
import 'package:collection/collection.dart';
66
import 'inputs/input.dart';
77
import 'inputs/taproot_input.dart';
8+
import 'locktime.dart';
89
import 'output.dart';
910
import 'transaction.dart';
1011

@@ -48,7 +49,7 @@ class CoinSelection {
4849
final BigInt feePerKb;
4950
final BigInt minFee;
5051
final BigInt minChange;
51-
final int locktime;
52+
final Locktime locktime;
5253

5354
/// The total value of selected inputs
5455
late final BigInt inputValue;
@@ -87,7 +88,7 @@ class CoinSelection {
8788
required this.feePerKb,
8889
required this.minFee,
8990
required this.minChange,
90-
this.locktime = 0,
91+
this.locktime = Locktime.zero,
9192
}) : selected = List.unmodifiable(selected),
9293
recipients = List.unmodifiable(recipients) {
9394

@@ -169,7 +170,7 @@ class CoinSelection {
169170
required BigInt feePerKb,
170171
required BigInt minFee,
171172
required BigInt minChange,
172-
int locktime = 0,
173+
Locktime locktime = Locktime.zero,
173174
}) {
174175

175176
final randomSelection = CoinSelection.random(
@@ -216,7 +217,7 @@ class CoinSelection {
216217
required BigInt feePerKb,
217218
required BigInt minFee,
218219
required BigInt minChange,
219-
int locktime = 0,
220+
Locktime locktime = Locktime.zero,
220221
bool randomise = false,
221222
int maxCandidates = 6800,
222223
}) {
@@ -258,7 +259,7 @@ class CoinSelection {
258259
required BigInt feePerKb,
259260
required BigInt minFee,
260261
required BigInt minChange,
261-
int locktime = 0,
262+
Locktime locktime = Locktime.zero,
262263
}) => CoinSelection.inOrderUntilEnough(
263264
version: version,
264265
candidates: candidates.toList()..shuffle(),
@@ -281,7 +282,7 @@ class CoinSelection {
281282
required BigInt feePerKb,
282283
required BigInt minFee,
283284
required BigInt minChange,
284-
int locktime = 0,
285+
Locktime locktime = Locktime.zero,
285286
}) => CoinSelection.inOrderUntilEnough(
286287
version: version,
287288
candidates: candidates.toList().sorted(

coinlib/lib/src/tx/locktime.dart

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import 'package:coinlib/src/common/checks.dart';
2+
import 'inputs/sequence.dart';
3+
4+
/// A locktime restricts when a transaction can be mined.
5+
///
6+
/// Either it is restricted to block heights that are equal or later than a
7+
/// given block height. For this use [BlockHeightLocktime].
8+
///
9+
/// Or it is restricted to where the median timestamp of the last 11 blocks is
10+
/// equal or later than a given timestamp. For this use [MedianTimeLocktime].
11+
///
12+
/// This restriction is ignored if all the transaction inputs are final with a
13+
/// [InputSequence.finalWithoutLocktime] sequence.
14+
sealed class Locktime {
15+
16+
static final int _timeThreshold = 500000000;
17+
18+
static const zero = BlockHeightLocktime._noCheck(0);
19+
20+
final int value;
21+
22+
const Locktime._noCheck(this.value);
23+
24+
Locktime._(this.value) {
25+
checkUint32(value);
26+
}
27+
28+
/// Creates a [BlockHeightLocktime] or [MedianTimeLocktime] from the raw
29+
/// value.
30+
factory Locktime(int value) {
31+
if (value < _timeThreshold) return BlockHeightLocktime(value);
32+
return MedianTimeLocktime._(value);
33+
}
34+
35+
/// Given the [medianTime] of the previous 11 blocks and the current
36+
/// [blockHeight], returns true if the locktime is reached and the transaction
37+
/// is unlocked.
38+
bool isUnlocked(DateTime medianTime, int blockHeight)
39+
=> this is BlockHeightLocktime
40+
? value <= blockHeight
41+
: (this as MedianTimeLocktime).time.compareTo(medianTime) <= 0;
42+
43+
}
44+
45+
class BlockHeightLocktime extends Locktime {
46+
47+
const BlockHeightLocktime._noCheck(super.height) : super._noCheck();
48+
49+
/// Restricts the transaction to block heights greater or equal to [height].
50+
///
51+
/// The [height] be less than 500000000.
52+
BlockHeightLocktime(int height) : super._(height) {
53+
if (height >= Locktime._timeThreshold) {
54+
throw ArgumentError.value(
55+
height, "height", "must be less than ${Locktime._timeThreshold}",
56+
);
57+
}
58+
}
59+
60+
}
61+
62+
class MedianTimeLocktime extends Locktime {
63+
64+
MedianTimeLocktime._(int value) : super._(value) {
65+
if (value < Locktime._timeThreshold) {
66+
throw ArgumentError.value(
67+
value, "value", "must be more or equal to ${Locktime._timeThreshold}",
68+
);
69+
}
70+
}
71+
72+
/// Restricts the transaction to blocks where the median timestamp of the
73+
/// previous 11 blocks is greater than or equal to [time].
74+
///
75+
/// Must be between "1985-11-05 00:53:20" and "2106-02-07 06:28:15".
76+
MedianTimeLocktime(
77+
DateTime time,
78+
) : this._(time.millisecondsSinceEpoch ~/ 1000);
79+
80+
DateTime get time => DateTime.fromMillisecondsSinceEpoch(value*1000);
81+
82+
}

coinlib/lib/src/tx/sighash/taproot_signature_hasher.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ final class TaprootSignatureHasher extends SignatureHasher with Writable {
4242

4343
// Total transaction data
4444
writer.writeUInt32(tx.version);
45-
writer.writeUInt32(tx.locktime);
45+
writer.writeUInt32(tx.locktime.value);
4646

4747
if (hashType.allInputs) {
4848
writer.writeSlice(txHashes.prevouts.singleHash);

coinlib/lib/src/tx/sighash/witness_signature_hasher.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ final class WitnessSignatureHasher extends SignatureHasher with Writable {
4444
writer.writeUInt64(details.value);
4545
writer.writeUInt32(thisInput.sequence.value);
4646
writer.writeSlice(hashOutputs);
47-
writer.writeUInt32(tx.locktime);
47+
writer.writeUInt32(tx.locktime.value);
4848
writer.writeUInt32(hashType.value);
4949

5050
}

coinlib/lib/src/tx/transaction.dart

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import 'package:coinlib/src/crypto/hash.dart';
77
import 'package:coinlib/src/tx/inputs/sequence.dart';
88
import 'package:coinlib/src/tx/inputs/taproot_key_input.dart';
99
import 'package:coinlib/src/tx/inputs/taproot_single_script_sig_input.dart';
10+
import 'package:coinlib/src/tx/locktime.dart';
1011
import 'inputs/input.dart';
1112
import 'inputs/input_signature.dart';
1213
import 'inputs/legacy_input.dart';
@@ -45,22 +46,24 @@ class Transaction with Writable {
4546
final int version;
4647
final List<Input> inputs;
4748
final List<Output> outputs;
48-
final int locktime;
49+
final Locktime locktime;
4950

5051
/// Constructs a transaction with the given [inputs] and [outputs].
5152
/// [TransactionTooLarge] will be thrown if the resulting transction exceeds
5253
/// [maxSize] (1MB).
54+
///
55+
/// To follow the behaviour of the reference Peercoin client, the [locktime]
56+
/// can be set to the current tip block height via [BlockHeightLocktime].
5357
Transaction({
5458
this.version = currentVersion,
5559
required Iterable<Input> inputs,
5660
required Iterable<Output> outputs,
57-
this.locktime = 0,
61+
this.locktime = Locktime.zero,
5862
})
5963
: inputs = List.unmodifiable(inputs),
6064
outputs = List.unmodifiable(outputs)
6165
{
6266
checkInt32(version);
63-
checkUint32(locktime);
6467
if (size > maxSize) throw TransactionTooLarge();
6568
}
6669

@@ -103,7 +106,7 @@ class Transaction with Writable {
103106
version: version,
104107
inputs: inputs,
105108
outputs: outputs,
106-
locktime: locktime,
109+
locktime: Locktime(locktime),
107110
);
108111

109112
}
@@ -182,7 +185,7 @@ class Transaction with Writable {
182185
}
183186
}
184187

185-
writer.writeUInt32(locktime);
188+
writer.writeUInt32(locktime.value);
186189

187190
}
188191

@@ -444,4 +447,15 @@ class Transaction with Writable {
444447
(input) => input.sequence.locktimeIsEnforced,
445448
);
446449

450+
/// Given the [medianTime] of the previous 11 blocks and the current
451+
/// [blockHeight], returns true if the transaction is unlocked and available
452+
/// for inclusion into a block.
453+
///
454+
/// This returns true if [locktimeIsEnforced] is false and otherwise checks
455+
/// the locktime.
456+
bool isUnlocked({
457+
required DateTime medianTime,
458+
required int blockHeight,
459+
}) => !locktimeIsEnforced || locktime.isUnlocked(medianTime, blockHeight);
460+
447461
}

coinlib/test/tx/coin_selection_test.dart

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ void main() {
171171
vector.outputValue + vector.expChangeValue,
172172
);
173173
expect(tx.version, Transaction.currentVersion);
174-
expect(tx.locktime, 0);
174+
expect(tx.locktime.value, 0);
175175

176176
} else {
177177
expect(
@@ -188,7 +188,7 @@ void main() {
188188

189189
final selection = CoinSelection(
190190
version: 1234,
191-
locktime: 54,
191+
locktime: Locktime(54),
192192
selected: [candidateForValue(coin)],
193193
recipients: [outputForValue(10000)],
194194
changeProgram: changeProgram,
@@ -199,7 +199,7 @@ void main() {
199199

200200
final tx = selection.transaction;
201201
expect(tx.version, 1234);
202-
expect(tx.locktime, 54);
202+
expect(tx.locktime.value, 54);
203203

204204
});
205205

@@ -278,7 +278,7 @@ void main() {
278278
unorderedEquals(values),
279279
);
280280
expect(selection.version, 1234);
281-
expect(selection.locktime, 0xabcd1234);
281+
expect(selection.locktime.value, 0xabcd1234);
282282
}
283283

284284
final candidates = [coin*4, coin, coin*3, coin, coin*2];
@@ -292,7 +292,7 @@ void main() {
292292
recipients: [outputForValue(outValue)],
293293
changeProgram: changeProgram,
294294
feePerKb: feePerKb, minFee: minFee, minChange: minChange,
295-
locktime: 0xabcd1234,
295+
locktime: Locktime(0xabcd1234),
296296
);
297297
expectSelectedValues(selection, selected);
298298
}
@@ -316,7 +316,7 @@ void main() {
316316
recipients: [outputForValue(outValue)],
317317
changeProgram: changeProgram,
318318
feePerKb: feePerKb, minFee: minFee, minChange: minChange,
319-
locktime: 0xabcd1234,
319+
locktime: Locktime(0xabcd1234),
320320
);
321321

322322
// Only need one
@@ -347,7 +347,7 @@ void main() {
347347
recipients: [outputForValue(outValue)],
348348
changeProgram: changeProgram,
349349
feePerKb: feePerKb, minFee: minFee, minChange: minChange,
350-
locktime: 0xabcd1234,
350+
locktime: Locktime(0xabcd1234),
351351
);
352352
expectSelectedValues(selection, selected);
353353
}
@@ -372,7 +372,7 @@ void main() {
372372
recipients: [outputForValue(outValue)],
373373
changeProgram: changeProgram,
374374
feePerKb: feePerKb, minFee: minFee, minChange: minChange,
375-
locktime: 0xabcd1234,
375+
locktime: Locktime(0xabcd1234),
376376
);
377377

378378
// Defaults to random where possible
@@ -397,7 +397,7 @@ void main() {
397397
expect(selection.tooLarge, false);
398398
expect(selection.enoughFunds, true);
399399
expect(selection.version, 1234);
400-
expect(selection.locktime, 0xabcd1234);
400+
expect(selection.locktime.value, 0xabcd1234);
401401
expect(
402402
selection.selected.where(
403403
(candidate) => candidate.value.toInt() == coin*100,

0 commit comments

Comments
 (0)