-
Notifications
You must be signed in to change notification settings - Fork 2.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
lnd+walletunlocker: make unlock/init operations synchronous #4349
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,6 +6,7 @@ import ( | |
"errors" | ||
"fmt" | ||
"os" | ||
"sync" | ||
"time" | ||
|
||
"github.com/btcsuite/btcd/chaincfg" | ||
|
@@ -54,6 +55,10 @@ type WalletInitMsg struct { | |
// ChanBackups a set of static channel backups that should be received | ||
// after the wallet has been initialized. | ||
ChanBackups ChannelsToRecover | ||
|
||
// Done is a channel that should be closed once the wallet has been | ||
// fully initialized. | ||
Done chan struct{} | ||
} | ||
|
||
// WalletUnlockMsg is a message sent by the UnlockerService when a user wishes | ||
|
@@ -81,6 +86,10 @@ type WalletUnlockMsg struct { | |
// ChanBackups a set of static channel backups that should be received | ||
// after the wallet has been unlocked. | ||
ChanBackups ChannelsToRecover | ||
|
||
// Done is a channel that should be closed once the wallet has been | ||
// fully unlocked. | ||
Done chan struct{} | ||
} | ||
|
||
// UnlockerService implements the WalletUnlocker service used to provide lnd | ||
|
@@ -100,18 +109,26 @@ type UnlockerService struct { | |
noFreelistSync bool | ||
netParams *chaincfg.Params | ||
macaroonFiles []string | ||
|
||
quitChan <-chan struct{} | ||
|
||
// This mutex is only used to guard concurrent access to the external | ||
// RPC calls. This ensures that we don't allow multiple callers to | ||
// init/unlock the wallet. | ||
sync.Mutex | ||
Roasbeef marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
// New creates and returns a new UnlockerService. | ||
func New(chainDir string, params *chaincfg.Params, noFreelistSync bool, | ||
macaroonFiles []string) *UnlockerService { | ||
macaroonFiles []string, quitChan <-chan struct{}) *UnlockerService { | ||
|
||
return &UnlockerService{ | ||
InitMsgs: make(chan *WalletInitMsg, 1), | ||
UnlockMsgs: make(chan *WalletUnlockMsg, 1), | ||
chainDir: chainDir, | ||
netParams: params, | ||
macaroonFiles: macaroonFiles, | ||
quitChan: quitChan, | ||
} | ||
} | ||
|
||
|
@@ -242,6 +259,9 @@ func extractChanBackups(chanBackups *lnrpc.ChanBackupSnapshot) *ChannelsToRecove | |
func (u *UnlockerService) InitWallet(ctx context.Context, | ||
in *lnrpc.InitWalletRequest) (*lnrpc.InitWalletResponse, error) { | ||
|
||
u.Lock() | ||
defer u.Unlock() | ||
|
||
// Make sure the password meets our constraints. | ||
password := in.WalletPassword | ||
if err := ValidatePassword(password); err != nil { | ||
|
@@ -293,6 +313,7 @@ func (u *UnlockerService) InitWallet(ctx context.Context, | |
Passphrase: password, | ||
WalletSeed: cipherSeed, | ||
RecoveryWindow: uint32(recoveryWindow), | ||
Done: make(chan struct{}), | ||
} | ||
|
||
// Before we return the unlock payload, we'll check if we can extract | ||
|
@@ -302,7 +323,19 @@ func (u *UnlockerService) InitWallet(ctx context.Context, | |
initMsg.ChanBackups = *chansToRestore | ||
} | ||
|
||
u.InitMsgs <- initMsg | ||
select { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. add helper to avoid having 3 copies of the same code? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. guess there are really two versions, one for init and one for unlock... |
||
case u.InitMsgs <- initMsg: | ||
case <-u.quitChan: | ||
return nil, fmt.Errorf("server shutting down") | ||
} | ||
|
||
// As we want to avoid a possible deadlock scenario, we'll wait the | ||
// daemon to respond that the wallet has been initialized. | ||
select { | ||
case <-initMsg.Done: | ||
case <-u.quitChan: | ||
return nil, fmt.Errorf("server shutting down") | ||
} | ||
|
||
return &lnrpc.InitWalletResponse{}, nil | ||
} | ||
|
@@ -313,6 +346,9 @@ func (u *UnlockerService) InitWallet(ctx context.Context, | |
func (u *UnlockerService) UnlockWallet(ctx context.Context, | ||
in *lnrpc.UnlockWalletRequest) (*lnrpc.UnlockWalletResponse, error) { | ||
|
||
u.Lock() | ||
defer u.Unlock() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we may also need another signal/bool at the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What do you mean? Push things down even further? With where things are atm, we won't return back to the caller until the wallet has been fully initialized. However, for unlock we return a bit earlier once we have all the credentials. In my testing, the concurrent init was was ended up tripping things up. For unlock, things only become "fully finalized" once we create the chain control. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually, as soon as this method returns, unlocking isn't even possible since the listener service only has a lifetime of this call. |
||
|
||
password := in.WalletPassword | ||
recoveryWindow := uint32(in.RecoveryWindow) | ||
|
||
|
@@ -346,6 +382,7 @@ func (u *UnlockerService) UnlockWallet(ctx context.Context, | |
Passphrase: password, | ||
RecoveryWindow: recoveryWindow, | ||
Wallet: unlockedWallet, | ||
Done: make(chan struct{}), | ||
} | ||
|
||
// Before we return the unlock payload, we'll check if we can extract | ||
|
@@ -358,7 +395,19 @@ func (u *UnlockerService) UnlockWallet(ctx context.Context, | |
// At this point we was able to open the existing wallet with the | ||
// provided password. We send the password over the UnlockMsgs | ||
// channel, such that it can be used by lnd to open the wallet. | ||
u.UnlockMsgs <- walletUnlockMsg | ||
select { | ||
case u.UnlockMsgs <- walletUnlockMsg: | ||
case <-u.quitChan: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is never closed. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah I see, the main shutdown chan is passed to it. |
||
return nil, fmt.Errorf("server shutting down") | ||
} | ||
|
||
// As we want to avoid a possible deadlock scenario, we'll wait the | ||
// daemon to respond that the wallet has been unlocked. | ||
select { | ||
case <-walletUnlockMsg.Done: | ||
case <-u.quitChan: | ||
return nil, fmt.Errorf("server shutting down") | ||
} | ||
|
||
return &lnrpc.UnlockWalletResponse{}, nil | ||
} | ||
|
@@ -369,6 +418,9 @@ func (u *UnlockerService) UnlockWallet(ctx context.Context, | |
func (u *UnlockerService) ChangePassword(ctx context.Context, | ||
in *lnrpc.ChangePasswordRequest) (*lnrpc.ChangePasswordResponse, error) { | ||
|
||
u.Lock() | ||
defer u.Unlock() | ||
|
||
netDir := btcwallet.NetworkDir(u.chainDir, u.netParams) | ||
loader := wallet.NewLoader(u.netParams, netDir, u.noFreelistSync, 0) | ||
|
||
|
@@ -431,7 +483,23 @@ func (u *UnlockerService) ChangePassword(ctx context.Context, | |
|
||
// Finally, send the new password across the UnlockPasswords channel to | ||
// automatically unlock the wallet. | ||
u.UnlockMsgs <- &WalletUnlockMsg{Passphrase: in.NewPassword} | ||
unlockMsg := &WalletUnlockMsg{ | ||
Passphrase: in.NewPassword, | ||
Done: make(chan struct{}), | ||
} | ||
select { | ||
case u.UnlockMsgs <- unlockMsg: | ||
case <-u.quitChan: | ||
return nil, fmt.Errorf("server shutting down") | ||
} | ||
|
||
// As we want to avoid a possible deadlock scenario, we'll wait the | ||
// daemon to respond that the wallet has been unlocked. | ||
select { | ||
case <-unlockMsg.Done: | ||
case <-u.quitChan: | ||
return nil, fmt.Errorf("server shutting down") | ||
} | ||
|
||
return &lnrpc.ChangePasswordResponse{}, nil | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was still able to reproduce the issue with the current changes. The issue doesn't seem to be related to the init call being asynchronous, but rather with the
UnlockerService
having a queuedUnlockWallet
call that it doesn't cancel afterInitWallet
has been called. If we had aUnlockerService.Quit
method, we could call it here and check that it's been closed after the lock has been acquired at theUnlockerService
level.