Skip to content

Commit c3554f9

Browse files
ConorOkusclaude
andcommitted
docs(advanced-guides): audit Key Management, Fee Estimation, Probing for LDK 0.2
- Probing: fix `Event::ailed` typo (-> Event::ProbeFailed) and update the outdated process_events_async call (add onion_messenger, liquidity manager, and output sweeper arguments). - Fee Estimation: bump the Cargo.toml example off 0.0.124 to lightning 0.2.2 / siblings 0.2.0 / lightning-invoice 0.34.0, edition 2021. (ConfirmationTarget coverage was already current.) - Key Management: add the new KeysManager v2_remote_key_derivation flag everywhere; update the custom SignerProvider wrapper to the 0.2 trait (EcdsaSigner, get_destination_script(channel_keys_id) -> ScriptBuf, drop channel_value_satoshis from generate_channel_keys_id/derive_channel_signer, spend_spendable_outputs via OutputSpender with ScriptBuf/Option<LockTime>); fix the events::Event path and docs.rs link; remove Swift tabs; add TypeScript to the simple init examples; link to LDK Node's production wrapper. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 925dd71 commit c3554f9

3 files changed

Lines changed: 78 additions & 196 deletions

File tree

docs/fee_estimation.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -106,16 +106,16 @@ Once you've created the directory, open the ```Cargo.toml``` file, which Cargo u
106106
[package]
107107
name = "ldk-fee-estimator"
108108
version = "0.1.0"
109-
edition = "2024"
109+
edition = "2021"
110110

111111
[dependencies]
112-
lightning = { version = "0.0.124", features = ["max_level_trace"] }
113-
lightning-block-sync = { version = "0.0.124", features = [ "rpc-client" ] }
114-
lightning-invoice = { version = "0.31.0" }
115-
lightning-net-tokio = { version = "0.0.124" }
116-
lightning-persister = { version = "0.0.124" }
117-
lightning-background-processor = { version = "0.0.124" }
118-
lightning-rapid-gossip-sync = { version = "0.0.124" }
112+
lightning = { version = "0.2.2", features = ["max_level_trace"] }
113+
lightning-block-sync = { version = "0.2.0", features = [ "rpc-client" ] }
114+
lightning-invoice = { version = "0.34.0" }
115+
lightning-net-tokio = { version = "0.2.0" }
116+
lightning-persister = { version = "0.2.0" }
117+
lightning-background-processor = { version = "0.2.0" }
118+
lightning-rapid-gossip-sync = { version = "0.2.0" }
119119
reqwest = { version = "0.11", features = ["json", "blocking"] }
120120
serde = { version = "1.0", features = ["derive"] }
121121
tokio = { version = "1", features = ["full"] } # Async runtime, required for reqwest

docs/key_management.md

Lines changed: 65 additions & 186 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Key Management
22

3-
LDK provides a simple default `KeysManager` implementation that takes a 32-byte seed for use as a BIP 32 extended key and derives keys from that. Check out the [Rust docs](https://docs.rs/lightning/*/lightning/sign/struct.KeysManager.html).
3+
LDK provides a simple default `KeysManager` implementation that takes a 32-byte seed for use as a BIP 32 extended key and derives keys from that. Check out the [Rust docs](https://docs.rs/lightning/0.2.2/lightning/sign/struct.KeysManager.html).
44

55
However, LDK also allows to customize the way key material and entropy are sourced through custom implementations of the `NodeSigner`, `SignerProvider`, and `EntropySource` traits located in `sign`. These traits include basic methods to provide public and private key material, as well as pseudorandom numbers.
66

@@ -12,28 +12,32 @@ A `KeysManager` can be constructed simply with only a 32-byte seed and some rand
1212
// Fill in random_32_bytes with secure random data, or, on restart, reload the seed from disk.
1313
let mut random_32_bytes = [0; 32];
1414
let start_time = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap();
15-
let keys_interface_impl = lightning::sign::KeysManager::new(&random_32_bytes, start_time.as_secs(), start_time.subsec_nanos());
15+
// 0.2 adds a `v2_remote_key_derivation` flag (pass `true` for new nodes).
16+
let keys_interface_impl = lightning::sign::KeysManager::new(&random_32_bytes, start_time.as_secs(), start_time.subsec_nanos(), true);
1617
```
1718

1819
```kotlin [Kotlin]
1920
// Fill in key_seed with secure random data, or, on restart, reload the seed from disk.
2021
val key_seed = ByteArray(32)
2122
val keys_manager = KeysManager.of(
2223
key_seed,
23-
System.currentTimeMillis() / 1000, (System.currentTimeMillis() * 1000).toInt()
24+
System.currentTimeMillis() / 1000, (System.currentTimeMillis() * 1000).toInt(),
25+
true // v2_remote_key_derivation
2426
)
2527
```
2628

27-
```swift [Swift]
28-
// Fill in seed with secure random data, or, on restart, reload the seed from disk.
29-
let seed = [UInt8](repeating: 0, count: 32)
30-
let timestampSeconds = UInt64(NSDate().timeIntervalSince1970)
31-
let timestampNanos = UInt32(truncating: NSNumber(value: timestampSeconds * 1000 * 1000))
32-
self.myKeysManager = KeysManager(
33-
seed: seed,
34-
startingTimeSecs: timestampSeconds,
35-
startingTimeNanos: timestampNanos
36-
)
29+
```typescript [TypeScript]
30+
import * as ldk from "lightningdevkit";
31+
32+
// Fill with secure random data, or, on restart, reload the seed from disk.
33+
const seed = new Uint8Array(32);
34+
const nowSecs = Math.floor(Date.now() / 1000);
35+
const keysManager = ldk.KeysManager.constructor_new(
36+
seed,
37+
BigInt(nowSecs), // starting_time_secs (bigint)
38+
(Date.now() % 1000) * 1_000_000, // starting_time_nanos
39+
true // v2_remote_key_derivation
40+
);
3741
```
3842

3943
:::
@@ -66,7 +70,7 @@ let ldk_seed: [u8; 32] = xprv.private_key.secret_bytes();
6670

6771
// Seed the LDK KeysManager with the private key at m/535h
6872
let cur = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap();
69-
let keys_manager = KeysManager::new(&ldk_seed, cur.as_secs(), cur.subsec_nanos());
73+
let keys_manager = KeysManager::new(&ldk_seed, cur.as_secs(), cur.subsec_nanos(), true);
7074
```
7175

7276
```kotlin [Kotlin]
@@ -83,29 +87,24 @@ val entropy: ByteArray = ldkChild.secretBytes().toUByteArray().toByteArray()
8387
val keysManager = KeysManager.of(
8488
entropy,
8589
System.currentTimeMillis() / 1000,
86-
(System.currentTimeMillis() * 1000).toInt()
90+
(System.currentTimeMillis() * 1000).toInt(),
91+
true // v2_remote_key_derivation
8792
);
8893
```
8994

90-
```swift [Swift]
91-
// Use BDK to create and build the HD wallet
92-
let mnemonic = try Mnemonic.fromString(mnemonic: "sock lyrics village put galaxy famous pass act ship second diagram pull")
93-
// Other supported networks include mainnet (Bitcoin), Regtest, Signet
94-
let bip32RootKey = DescriptorSecretKey(network: .testnet, mnemonic: mnemonic, password: nil)
95-
let ldkDerivationPath = try DerivationPath(path: "m/535h")
96-
let ldkChild = try bip32RootKey.derive(path: ldkDerivationPath)
97-
let ldkSeed = ldkChild.secretBytes()
98-
99-
// Retrieve the current system time for uniqueness across restarts.
100-
let timestampSeconds = UInt64(NSDate().timeIntervalSince1970)
101-
let timestampNanos = UInt32(truncating: NSNumber(value: timestampSeconds * 1000 * 1000))
102-
103-
// Seed the LDK KeysManager with the private key at m/535h
104-
let keysManager = KeysManager(
105-
seed: ldkSeed,
106-
startingTimeSecs: timestampSeconds,
107-
startingTimeNanos: timestampNanos
108-
)
95+
```typescript [TypeScript]
96+
import * as ldk from "lightningdevkit";
97+
98+
// Derive your 32-byte LDK seed from your HD wallet (e.g. the key at m/535h),
99+
// then seed the KeysManager with it.
100+
const ldkSeed: Uint8Array = /* 32 bytes derived at m/535h */;
101+
const nowSecs = Math.floor(Date.now() / 1000);
102+
const keysManager = ldk.KeysManager.constructor_new(
103+
ldkSeed,
104+
BigInt(nowSecs),
105+
(Date.now() % 1000) * 1_000_000,
106+
true // v2_remote_key_derivation
107+
);
109108
```
110109

111110
:::
@@ -118,7 +117,7 @@ An advantage to this approach is that the LDK entropy is contained within your i
118117

119118
# Spending On-Chain Funds
120119

121-
When a channel has been closed and some outputs on chain are spendable only by us, LDK provides a `util::events::Event::SpendableOutputs` event in return from `ChannelMonitor::get_and_clear_pending_events()`. It contains a list of `sign::SpendableOutputDescriptor` objects which describe the output and provide all necessary information to spend it.
120+
When a channel has been closed and some outputs on chain are spendable only by us, LDK provides a `events::Event::SpendableOutputs` event in return from `ChannelMonitor::get_and_clear_pending_events()`. It contains a list of `sign::SpendableOutputDescriptor` objects which describe the output and provide all necessary information to spend it.
122121

123122
If you're using `KeysManager` directly, a utility method is provided which can generate a signed transaction given a list of `
124123
SpendableOutputDescriptor` objects. `KeysManager::spend_spendable_outputs` can be called any time after receiving the `SpendableOutputDescriptor` objects to build a spending transaction, including delaying until sending funds to an external destination or opening a new channel. Note that if you open new channels directly with `SpendableOutputDescriptor` objects, you must ensure all closing/destination scripts provided to LDK are SegWit (either native or P2SH-wrapped).
@@ -129,6 +128,15 @@ In order to make the outputs from channel closing spendable by a third-party wal
129128

130129
For example, a wrapper based on BDK's [`Wallet`](https://docs.rs/bdk/*/bdk/wallet/struct.Wallet.html) could look like this:
131130

131+
::: tip Note
132+
The `SignerProvider` trait changed in 0.2: `generate_channel_keys_id` and
133+
`derive_channel_signer` no longer take `channel_value_satoshis`,
134+
`get_destination_script` now takes a `channel_keys_id` and returns `ScriptBuf`,
135+
and `spend_spendable_outputs` lives on the separate `OutputSpender` trait
136+
(implemented by `KeysManager`). For a production-grade wrapper, see
137+
[LDK Node's `KeysManager`](https://github.com/lightningdevkit/ldk-node/blob/main/src/wallet/mod.rs).
138+
:::
139+
132140
::: code-group
133141

134142
```rust [Rust]
@@ -147,7 +155,7 @@ where
147155
pub fn new(
148156
seed: &[u8; 32], starting_time_secs: u64, starting_time_nanos: u32, wallet: Arc<Mutex<bdk::Wallet<D>>>,
149157
) -> Self {
150-
let inner = KeysManager::new(seed, starting_time_secs, starting_time_nanos);
158+
let inner = KeysManager::new(seed, starting_time_secs, starting_time_nanos, true);
151159
Self { inner, wallet }
152160
}
153161

@@ -159,8 +167,8 @@ where
159167
// See https://bitcoinops.org/en/topics/fee-sniping/ for more information.
160168
pub fn spend_spendable_outputs<C: Signing>(
161169
&self, descriptors: &[&SpendableOutputDescriptor], outputs: Vec<TxOut>,
162-
change_destination_script: Script, feerate_sat_per_1000_weight: u32,
163-
locktime: Option<PackedLockTime>, secp_ctx: &Secp256k1<C>,
170+
change_destination_script: ScriptBuf, feerate_sat_per_1000_weight: u32,
171+
locktime: Option<LockTime>, secp_ctx: &Secp256k1<C>,
164172
) -> Result<Transaction, ()> {
165173
let only_non_static = &descriptors
166174
.iter()
@@ -188,10 +196,11 @@ impl<D> SignerProvider for BDKKeysManager<D>
188196
where
189197
D: bdk::database::BatchDatabase,
190198
{
191-
type Signer = InMemorySigner;
199+
type EcdsaSigner = InMemorySigner;
192200

193201
// We return the destination and shutdown scripts derived by the BDK wallet.
194-
fn get_destination_script(&self) -> Result<Script, ()> {
202+
// `get_destination_script` now takes a `channel_keys_id` and returns `ScriptBuf`.
203+
fn get_destination_script(&self, _channel_keys_id: [u8; 32]) -> Result<ScriptBuf, ()> {
195204
let address = self.wallet.lock().unwrap()
196205
.get_address(bdk::wallet::AddressIndex::New)
197206
.map_err(|e| {
@@ -217,20 +226,18 @@ where
217226
}
218227

219228
// ... and redirect all other trait method implementations to the `inner` `KeysManager`.
229+
// Note: `generate_channel_keys_id` and `derive_channel_signer` no longer take
230+
// `channel_value_satoshis` in 0.2.
220231
fn generate_channel_keys_id(
221-
&self, inbound: bool, channel_value_satoshis: u64, user_channel_id: u128,
232+
&self, inbound: bool, user_channel_id: u128,
222233
) -> [u8; 32] {
223-
self.inner.generate_channel_keys_id(inbound, channel_value_satoshis, user_channel_id)
234+
self.inner.generate_channel_keys_id(inbound, user_channel_id)
224235
}
225236

226237
fn derive_channel_signer(
227-
&self, channel_value_satoshis: u64, channel_keys_id: [u8; 32],
228-
) -> Self::Signer {
229-
self.inner.derive_channel_signer(channel_value_satoshis, channel_keys_id)
230-
}
231-
232-
fn read_chan_signer(&self, reader: &[u8]) -> Result<Self::Signer, DecodeError> {
233-
self.inner.read_chan_signer(reader)
238+
&self, channel_keys_id: [u8; 32],
239+
) -> Self::EcdsaSigner {
240+
self.inner.derive_channel_signer(channel_keys_id)
234241
}
235242
}
236243

@@ -257,7 +264,7 @@ class LDKKeysManager(seed: ByteArray, startTimeSecs: Long, startTimeNano: Int, w
257264
var signerProvider: LDKSignerProvider
258265

259266
init {
260-
this.inner = KeysManager.of(seed, startTimeSecs, startTimeNano)
267+
this.inner = KeysManager.of(seed, startTimeSecs, startTimeNano, true)
261268
this.wallet = wallet
262269
signerProvider = LDKSignerProvider()
263270
signerProvider.ldkkeysManager = this
@@ -291,21 +298,20 @@ class LDKKeysManager(seed: ByteArray, startTimeSecs: Long, startTimeNano: Int, w
291298
class LDKSignerProvider : SignerProvider.SignerProviderInterface {
292299
var ldkkeysManager: LDKKeysManager? = null
293300

294-
override fun generate_channel_keys_id(inbound: Boolean, channelValueSatoshis: Long, userChannelId: UInt128?): ByteArray {
295-
return ldkkeysManager!!.inner.as_SignerProvider().generate_channel_keys_id(inbound, channelValueSatoshis, userChannelId)
296-
}
297-
298-
override fun derive_channel_signer(channelValueSatoshis: Long, channelKeysId: ByteArray?): WriteableEcdsaChannelSigner {
299-
return ldkkeysManager!!.inner.as_SignerProvider().derive_channel_signer(channelValueSatoshis, channelKeysId)
301+
// Note: `generate_channel_keys_id` and `derive_channel_signer` no longer take
302+
// `channelValueSatoshis` in 0.2.
303+
override fun generate_channel_keys_id(inbound: Boolean, userChannelId: UInt128?): ByteArray {
304+
return ldkkeysManager!!.inner.as_SignerProvider().generate_channel_keys_id(inbound, userChannelId)
300305
}
301306

302-
override fun read_chan_signer(reader: ByteArray?): Result_WriteableEcdsaChannelSignerDecodeErrorZ {
303-
return ldkkeysManager!!.inner.as_SignerProvider().read_chan_signer(reader)
307+
override fun derive_channel_signer(channelKeysId: ByteArray?): EcdsaChannelSigner {
308+
return ldkkeysManager!!.inner.as_SignerProvider().derive_channel_signer(channelKeysId)
304309
}
305310

306311
// We return the destination and shutdown scripts derived by the BDK wallet.
312+
// `get_destination_script` now takes a `channelKeysId`.
307313
@OptIn(ExperimentalUnsignedTypes::class)
308-
override fun get_destination_script(): Result_CVec_u8ZNoneZ {
314+
override fun get_destination_script(channelKeysId: ByteArray): Result_CVec_u8ZNoneZ {
309315
val address = ldkkeysManager!!.wallet.getAddress(AddressIndex.New)
310316
return Result_CVec_u8ZNoneZ.ok(address.address.scriptPubkey().toBytes().toUByteArray().toByteArray())
311317
}
@@ -336,131 +342,4 @@ class LDKSignerProvider : SignerProvider.SignerProviderInterface {
336342

337343
```
338344

339-
```swift [Swift]
340-
class MyKeysManager {
341-
let inner: KeysManager
342-
let wallet: BitcoinDevKit.Wallet
343-
let signerProvider: MySignerProvider
344-
345-
init(seed: [UInt8], startingTimeSecs: UInt64, startingTimeNanos: UInt32, wallet: BitcoinDevKit.Wallet) {
346-
self.inner = KeysManager(seed: seed, startingTimeSecs: startingTimeSecs, startingTimeNanos: startingTimeNanos)
347-
self.wallet = wallet
348-
signerProvider = MySignerProvider()
349-
signerProvider.myKeysManager = self
350-
}
351-
352-
// We drop all occurences of `SpendableOutputDescriptor::StaticOutput` (since they will be
353-
// spendable by the BDK wallet) and forward any other descriptors to
354-
// `KeysManager::spend_spendable_outputs`.
355-
//
356-
// Note you should set `locktime` to the current block height to mitigate fee sniping.
357-
// See https://bitcoinops.org/en/topics/fee-sniping/ for more information.
358-
func spendSpendableOutputs(descriptors: [SpendableOutputDescriptor], outputs: [Bindings.TxOut],
359-
changeDestinationScript: [UInt8], feerateSatPer1000Weight: UInt32,
360-
locktime: UInt32?) -> Result_TransactionNoneZ {
361-
let onlyNonStatic: [SpendableOutputDescriptor] = descriptors.filter { desc in
362-
if desc.getValueType() == .StaticOutput {
363-
return false
364-
}
365-
return true
366-
}
367-
let res = self.inner.spendSpendableOutputs(
368-
descriptors: onlyNonStatic,
369-
outputs: outputs,
370-
changeDestinationScript: changeDestinationScript,
371-
feerateSatPer1000Weight: feerateSatPer1000Weight,
372-
locktime: locktime
373-
)
374-
return res
375-
}
376-
}
377-
378-
class MySignerProvider: SignerProvider {
379-
weak var myKeysManager: MyKeysManager?
380-
381-
// We return the destination and shutdown scripts derived by the BDK wallet.
382-
override func getDestinationScript() -> Bindings.Result_ScriptNoneZ {
383-
do {
384-
let address = try myKeysManager!.wallet.getAddress(addressIndex: .new)
385-
return Bindings.Result_ScriptNoneZ.initWithOk(o: address.address.scriptPubkey().toBytes())
386-
} catch {
387-
return .initWithErr()
388-
}
389-
}
390-
391-
override func getShutdownScriptpubkey() -> Bindings.Result_ShutdownScriptNoneZ {
392-
do {
393-
let address = try myKeysManager!.wallet.getAddress(addressIndex: .new).address
394-
let payload = address.payload()
395-
if case let .witnessProgram(`version`, `program`) = payload {
396-
let ver: UInt8
397-
switch version {
398-
case .v0:
399-
ver = 0
400-
case .v1:
401-
ver = 1
402-
case .v2:
403-
ver = 2
404-
case .v3:
405-
ver = 3
406-
case .v4:
407-
ver = 4
408-
case .v5:
409-
ver = 5
410-
case .v6:
411-
ver = 6
412-
case .v7:
413-
ver = 7
414-
case .v8:
415-
ver = 8
416-
case .v9:
417-
ver = 9
418-
case .v10:
419-
ver = 10
420-
case .v11:
421-
ver = 11
422-
case .v12:
423-
ver = 12
424-
case .v13:
425-
ver = 13
426-
case .v14:
427-
ver = 14
428-
case .v15:
429-
ver = 15
430-
case .v16:
431-
ver = 16
432-
}
433-
let res = ShutdownScript.newWitnessProgram(version: ver, program: program)
434-
if res.isOk() {
435-
return Bindings.Result_ShutdownScriptNoneZ.initWithOk(o: res.getValue()!)
436-
}
437-
}
438-
return .initWithErr()
439-
} catch {
440-
return .initWithErr()
441-
}
442-
}
443-
444-
// ... and redirect all other trait method implementations to the `inner` `KeysManager`.
445-
override func deriveChannelSigner(channelValueSatoshis: UInt64, channelKeysId: [UInt8]) -> Bindings.WriteableEcdsaChannelSigner {
446-
return myKeysManager!.inner.asSignerProvider().deriveChannelSigner(
447-
channelValueSatoshis: channelValueSatoshis,
448-
channelKeysId: channelKeysId
449-
)
450-
}
451-
452-
override func generateChannelKeysId(inbound: Bool, channelValueSatoshis: UInt64, userChannelId: [UInt8]) -> [UInt8] {
453-
return myKeysManager!.inner.asSignerProvider().generateChannelKeysId(
454-
inbound: inbound,
455-
channelValueSatoshis: channelValueSatoshis,
456-
userChannelId: userChannelId
457-
)
458-
}
459-
460-
override func readChanSigner(reader: [UInt8]) -> Bindings.Result_WriteableEcdsaChannelSignerDecodeErrorZ {
461-
return myKeysManager!.inner.asSignerProvider().readChanSigner(reader: reader)
462-
}
463-
}
464-
```
465-
466345
:::

0 commit comments

Comments
 (0)