Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions cmd/commands/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ func actionDecorator(f func(*cli.Context) error) func(*cli.Context) error {
// 'unlock') but the wallet is already unlocked, then these
// methods aren't recognized any more because this service is
// shut down after successful unlock.
if s.Code() == codes.Unknown && strings.Contains(
if s.Code() == codes.FailedPrecondition && strings.Contains(
s.Message(), rpcperms.ErrWalletUnlocked.Error(),
) && (c.Command.Name == "create" ||
c.Command.Name == "unlock" ||
Expand All @@ -283,7 +283,7 @@ func actionDecorator(f func(*cli.Context) error) func(*cli.Context) error {

// lnd might be active, but not possible to contact using RPC if
// the wallet is encrypted.
if s.Code() == codes.Unknown && strings.Contains(
if s.Code() == codes.FailedPrecondition && strings.Contains(
s.Message(), rpcperms.ErrWalletLocked.Error(),
) {

Expand Down
8 changes: 8 additions & 0 deletions docs/release-notes/release-notes-0.22.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,14 @@

## RPC Updates

* The RPC state interceptor now [returns structured gRPC status
codes](https://github.com/lightningnetwork/lnd/pull/10945) instead of plain
Go errors. `ErrWaitingToStart` and `ErrRPCStarting` map to
`codes.Unavailable`; `ErrNoWallet`, `ErrWalletLocked`, and
`ErrWalletUnlocked` map to `codes.FailedPrecondition`. Callers can now
branch on the status code directly rather than performing string matching
against the error message.

## lncli Updates

## Breaking Changes
Expand Down
26 changes: 19 additions & 7 deletions rpcperms/interceptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -878,43 +878,55 @@ func (r *InterceptorChain) checkRPCState(srv interface{}) error {
// Do not accept any RPC calls (unless to the state service) until LND
// has not started.
case waitingToStart:
return ErrWaitingToStart
return status.Error(codes.Unavailable, ErrWaitingToStart.Error())

// If the wallet does not exists, only calls to the WalletUnlocker are
// accepted.
case walletNotCreated:
_, ok := srv.(lnrpc.WalletUnlockerServer)
if !ok {
return ErrNoWallet
return status.Error(
codes.FailedPrecondition, ErrNoWallet.Error(),
)
}

// If the wallet is locked, only calls to the WalletUnlocker are
// accepted.
case walletLocked:
_, ok := srv.(lnrpc.WalletUnlockerServer)
if !ok {
return ErrWalletLocked
return status.Error(
codes.FailedPrecondition, ErrWalletLocked.Error(),
)
}

// If the wallet is unlocked, but the RPC not yet active, we reject.
case walletUnlocked:
_, ok := srv.(lnrpc.WalletUnlockerServer)
if ok {
return ErrWalletUnlocked
return status.Error(
codes.FailedPrecondition,
ErrWalletUnlocked.Error(),
)
}

return ErrRPCStarting
return status.Error(codes.Unavailable, ErrRPCStarting.Error())

// If the RPC server or lnd server is active, we allow calls to any
// service except the WalletUnlocker.
case rpcActive, serverActive:
_, ok := srv.(lnrpc.WalletUnlockerServer)
if ok {
return ErrWalletUnlocked
return status.Error(
codes.FailedPrecondition,
ErrWalletUnlocked.Error(),
)
}

default:
return fmt.Errorf("unknown RPC state: %v", state)
return status.Errorf(
codes.Unknown, "unknown RPC state: %v", state,
)
}

return nil
Expand Down
108 changes: 108 additions & 0 deletions rpcperms/interceptor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"testing"

"github.com/btcsuite/btclog/v2"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/stretchr/testify/require"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
Expand Down Expand Up @@ -111,6 +112,113 @@ func (s *recordingServerStream) SendMsg(any) error {
return nil
}

// mockUnlockerServer satisfies lnrpc.WalletUnlockerServer so it can be used
// as the server argument in checkRPCState tests.
type mockUnlockerServer struct {
lnrpc.UnimplementedWalletUnlockerServer
}

// mockOtherServer is a server that does NOT implement WalletUnlockerServer.
type mockOtherServer struct{}

// TestCheckRPCStateGRPCCodes verifies that checkRPCState returns errors with
// the correct gRPC status codes so callers can branch on codes rather than
// performing brittle string matching.
func TestCheckRPCStateGRPCCodes(t *testing.T) {
t.Parallel()

unlocker := &mockUnlockerServer{}
other := &mockOtherServer{}

chain := &InterceptorChain{}

cases := []struct {
name string
state rpcState
srv interface{}
wantCode codes.Code
wantErrNil bool
}{
{
name: "waitingToStart returns Unavailable",
state: waitingToStart,
srv: other,
wantCode: codes.Unavailable,
},
{
name: "walletNotCreated non-unlocker returns FailedPrecondition",
state: walletNotCreated,
srv: other,
wantCode: codes.FailedPrecondition,
},
{
name: "walletNotCreated unlocker returns nil",
state: walletNotCreated,
srv: unlocker,
wantErrNil: true,
},
{
name: "walletLocked non-unlocker returns FailedPrecondition",
state: walletLocked,
srv: other,
wantCode: codes.FailedPrecondition,
},
{
name: "walletLocked unlocker returns nil",
state: walletLocked,
srv: unlocker,
wantErrNil: true,
},
{
name: "walletUnlocked unlocker returns FailedPrecondition",
state: walletUnlocked,
srv: unlocker,
wantCode: codes.FailedPrecondition,
},
{
name: "walletUnlocked non-unlocker returns Unavailable",
state: walletUnlocked,
srv: other,
wantCode: codes.Unavailable,
},
{
name: "rpcActive unlocker returns FailedPrecondition",
state: rpcActive,
srv: unlocker,
wantCode: codes.FailedPrecondition,
},
{
name: "rpcActive non-unlocker returns nil",
state: rpcActive,
srv: other,
wantErrNil: true,
},
{
name: "serverActive non-unlocker returns nil",
state: serverActive,
srv: other,
wantErrNil: true,
},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()

chain.state = tc.state
err := chain.checkRPCState(tc.srv)

if tc.wantErrNil {
require.NoError(t, err)
return
}

require.Error(t, err)
require.Equal(t, tc.wantCode, status.Code(err))
})
}
}

// TestTruncatePanicStack asserts that panic stack traces are capped with a
// readable truncation marker.
func TestTruncatePanicStack(t *testing.T) {
Expand Down
Loading