lnwallet: add LastUnusedAddress to WalletController interface

In this commit, we add a new `LastUnusedAddress` method to the
`WalletController` interface. Callers can use this new method to graph
the last unused address, which can be useful for UIs that want to
refresh the address, but not cause nearly unbounded address generation.

The implementation for `btcwallet` uses the existing `CurrentAddress`
method. We've also added a new integration tests to exercise the new
functionality.
This commit is contained in:
Olaoluwa Osuntokun 2019-02-19 19:16:39 -08:00
parent 7703567b0b
commit 8c1181af3b
No known key found for this signature in database
GPG Key ID: CE58F7F8E20FD9A2
3 changed files with 91 additions and 0 deletions

@ -254,6 +254,29 @@ func (b *BtcWallet) NewAddress(t lnwallet.AddressType, change bool) (btcutil.Add
return b.wallet.NewAddress(defaultAccount, keyScope) return b.wallet.NewAddress(defaultAccount, keyScope)
} }
// LastUnusedAddress returns the last *unused* address known by the wallet. An
// address is unused if it hasn't received any payments. This can be useful in
// UIs in order to continually show the "freshest" address without having to
// worry about "address inflation" caused by continual refreshing. Similar to
// NewAddress it can derive a specified address type, and also optionally a
// change address.
func (b *BtcWallet) LastUnusedAddress(addrType lnwallet.AddressType) (
btcutil.Address, error) {
var keyScope waddrmgr.KeyScope
switch addrType {
case lnwallet.WitnessPubKey:
keyScope = waddrmgr.KeyScopeBIP0084
case lnwallet.NestedWitnessPubKey:
keyScope = waddrmgr.KeyScopeBIP0049Plus
default:
return nil, fmt.Errorf("unknown address type")
}
return b.wallet.CurrentAddress(defaultAccount, keyScope)
}
// IsOurAddress checks if the passed address belongs to this wallet // IsOurAddress checks if the passed address belongs to this wallet
// //
// This is a part of the WalletController interface. // This is a part of the WalletController interface.

@ -150,6 +150,14 @@ type WalletController interface {
// p2wsh, etc. // p2wsh, etc.
NewAddress(addrType AddressType, change bool) (btcutil.Address, error) NewAddress(addrType AddressType, change bool) (btcutil.Address, error)
// LastUnusedAddress returns the last *unused* address known by the
// wallet. An address is unused if it hasn't received any payments.
// This can be useful in UIs in order to continually show the
// "freshest" address without having to worry about "address inflation"
// caused by continual refreshing. Similar to NewAddress it can derive
// a specified address type. By default, this is a non-change address.
LastUnusedAddress(addrType AddressType) (btcutil.Address, error)
// IsOurAddress checks if the passed address belongs to this wallet // IsOurAddress checks if the passed address belongs to this wallet
IsOurAddress(a btcutil.Address) bool IsOurAddress(a btcutil.Address) bool

@ -2165,6 +2165,62 @@ func testChangeOutputSpendConfirmation(r *rpctest.Harness,
} }
} }
// testLastUnusedAddr tests that the LastUnusedAddress returns the address if
// it isn't used, and also that once the address becomes used, then it's
// properly rotated.
func testLastUnusedAddr(miner *rpctest.Harness,
alice, bob *lnwallet.LightningWallet, t *testing.T) {
if _, err := miner.Node.Generate(1); err != nil {
t.Fatalf("unable to generate block: %v", err)
}
// We'll repeat this test for each address type to ensure they're all
// rotated properly.
addrTypes := []lnwallet.AddressType{
lnwallet.WitnessPubKey, lnwallet.NestedWitnessPubKey,
}
for _, addrType := range addrTypes {
addr1, err := alice.LastUnusedAddress(addrType)
if err != nil {
t.Fatalf("unable to get addr: %v", err)
}
addr2, err := alice.LastUnusedAddress(addrType)
if err != nil {
t.Fatalf("unable to get addr: %v", err)
}
// If we generate two addresses back to back, then we should
// get the same addr, as none of them have been used yet.
if addr1.String() != addr2.String() {
t.Fatalf("addresses changed w/o use: %v vs %v", addr1, addr2)
}
// Next, we'll have Bob pay to Alice's new address. This should
// trigger address rotation at the backend wallet.
addrScript, err := txscript.PayToAddrScript(addr1)
if err != nil {
t.Fatalf("unable to convert addr to script: %v", err)
}
feeRate := lnwallet.SatPerKWeight(2500)
output := &wire.TxOut{
Value: 1000000,
PkScript: addrScript,
}
sendCoins(t, miner, bob, alice, output, feeRate)
// If we make a new address, then it should be brand new, as
// the prior address has been used.
addr3, err := alice.LastUnusedAddress(addrType)
if err != nil {
t.Fatalf("unable to get addr: %v", err)
}
if addr1.String() == addr3.String() {
t.Fatalf("address should have changed but didn't")
}
}
}
type walletTestCase struct { type walletTestCase struct {
name string name string
test func(miner *rpctest.Harness, alice, bob *lnwallet.LightningWallet, test func(miner *rpctest.Harness, alice, bob *lnwallet.LightningWallet,
@ -2219,6 +2275,10 @@ var walletTests = []walletTestCase{
name: "test cancel non-existent reservation", name: "test cancel non-existent reservation",
test: testCancelNonExistentReservation, test: testCancelNonExistentReservation,
}, },
{
name: "last unused addr",
test: testLastUnusedAddr,
},
{ {
name: "reorg wallet balance", name: "reorg wallet balance",
test: testReorgWalletBalance, test: testReorgWalletBalance,