Skip to content

Commit 2c2d6e3

Browse files
committed
feat: basic MWEB transaction read and write
Support reading and writing transactions carrying MWEB extension data as used by Litecoin. The 0x08 flag bit alongside the witness marker signals MWEB data, which is carried as opaque bytes between the witness data and the locktime. mwebBytes is preserved through input/output modification copies and legacy sighash computation. Ported from cypherstack/coinlib commit 7723ba3, reworked to keep the existing expectWitness two-pass parsing API and adapted to the Locktime abstraction.
1 parent f373c29 commit 2c2d6e3

2 files changed

Lines changed: 36 additions & 6 deletions

File tree

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ final class LegacySignatureHasher extends SignatureHasher {
7171
inputs: modifiedInputs,
7272
outputs: modifiedOutputs,
7373
locktime: tx.locktime,
74+
mwebBytes: tx.mwebBytes,
7475
);
7576

7677
// Add sighash type onto the end

coinlib/lib/src/tx/transaction.dart

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ class Transaction with Writable {
4747
final List<Input> inputs;
4848
final List<Output> outputs;
4949
final Locktime locktime;
50+
/// Raw MWEB extension data carried between the witness data and locktime,
51+
/// as used by Litecoin. This is kept as opaque bytes.
52+
final Uint8List? mwebBytes;
5053

5154
/// Constructs a transaction with the given [inputs] and [outputs].
5255
/// [TransactionTooLarge] will be thrown if the resulting transction exceeds
@@ -59,6 +62,7 @@ class Transaction with Writable {
5962
required Iterable<Input> inputs,
6063
required Iterable<Output> outputs,
6164
this.locktime = Locktime.zero,
65+
this.mwebBytes,
6266
})
6367
: inputs = List.unmodifiable(inputs),
6468
outputs = List.unmodifiable(outputs)
@@ -77,11 +81,17 @@ class Transaction with Writable {
7781

7882
final version = reader.readInt32();
7983

84+
bool hasWitness = false;
85+
bool hasMweb = false;
8086
if (witness) {
81-
// Check for witness data
87+
// Check for witness data. The 0x01 flag bit signals witness data and
88+
// the 0x08 flag bit signals MWEB data as used by Litecoin.
8289
final marker = reader.readUInt8();
8390
final flag = reader.readUInt8();
84-
if (marker != 0 || flag != 1) return null;
91+
if (marker != 0) return null;
92+
hasWitness = (flag & 1) != 0;
93+
hasMweb = (flag & 8) != 0;
94+
if (!hasWitness && !hasMweb) return null;
8595
}
8696

8797
final rawInputs = List.generate(
@@ -96,17 +106,27 @@ class Transaction with Writable {
96106

97107
// Match the raw inputs with witness data if this is a witness transaction
98108
final inputs = rawInputs.map(
99-
(raw) => Input.match(raw, witness ? reader.readVector() : []),
109+
(raw) => Input.match(raw, hasWitness ? reader.readVector() : []),
100110
// Create list now to ensure we read the witness data before the locktime
101111
).toList();
102112

113+
// Any MWEB data is carried between the witness data and the locktime
114+
Uint8List? mwebBytes;
115+
if (hasMweb) {
116+
final remaining = reader.bytes.lengthInBytes - reader.offset;
117+
if (remaining > 4) {
118+
mwebBytes = reader.readSlice(remaining - 4);
119+
}
120+
}
121+
103122
final locktime = reader.readUInt32();
104123

105124
return Transaction(
106125
version: version,
107126
inputs: inputs,
108127
outputs: outputs,
109128
locktime: Locktime(locktime),
129+
mwebBytes: mwebBytes,
110130
);
111131

112132
}
@@ -166,7 +186,7 @@ class Transaction with Writable {
166186

167187
if (isWitness) {
168188
writer.writeUInt8(0); // Marker
169-
writer.writeUInt8(1); // Flag
189+
writer.writeUInt8(mwebBytes != null ? 9 : 1); // Flag
170190
}
171191

172192
writer.writeVarInt(BigInt.from(inputs.length));
@@ -185,6 +205,10 @@ class Transaction with Writable {
185205
}
186206
}
187207

208+
if (mwebBytes != null) {
209+
writer.writeSlice(mwebBytes!);
210+
}
211+
188212
writer.writeUInt32(locktime.value);
189213

190214
}
@@ -194,6 +218,7 @@ class Transaction with Writable {
194218
inputs: newInputs,
195219
outputs: outputs,
196220
locktime: locktime,
221+
mwebBytes: mwebBytes,
197222
);
198223

199224
T _requireInputOfType<T>(int inputN) {
@@ -346,6 +371,7 @@ class Transaction with Writable {
346371
],
347372
outputs: outputs,
348373
locktime: locktime,
374+
mwebBytes: mwebBytes,
349375
);
350376

351377
/// Returns a new [Transaction] with the [output] added to the end of the
@@ -370,6 +396,7 @@ class Transaction with Writable {
370396
inputs: modifiedInputs,
371397
outputs: [...outputs, output],
372398
locktime: locktime,
399+
mwebBytes: mwebBytes,
373400
);
374401

375402
}
@@ -394,6 +421,7 @@ class Transaction with Writable {
394421
),
395422
outputs: outputs,
396423
locktime: locktime,
424+
mwebBytes: mwebBytes,
397425
)
398426
: this;
399427

@@ -414,8 +442,9 @@ class Transaction with Writable {
414442
String get txid
415443
=> bytesToHex(Uint8List.fromList(legacyHash.reversed.toList()));
416444

417-
/// If the transaction has any witness inputs.
418-
bool get isWitness => inputs.any((input) => input is WitnessInput);
445+
/// If the transaction has any witness inputs or carries MWEB data.
446+
bool get isWitness
447+
=> inputs.any((input) => input is WitnessInput) || mwebBytes != null;
419448

420449
bool get isCoinBase
421450
=> inputs.length == 1

0 commit comments

Comments
 (0)