diff --git a/auctioneer/client.go b/auctioneer/client.go index 38bca86..6d1e75c 100644 --- a/auctioneer/client.go +++ b/auctioneer/client.go @@ -490,25 +490,50 @@ func (c *Client) connectAndAuthenticate(ctx context.Context, } // Did the server find the account we're interested in? - switch msg := srvMsg.Msg.(type) { + switch { // Account exists, everything's good to continue. - case *clmrpc.ServerAuctionMessage_Success: + case srvMsg.GetSuccess() != nil: return sub, true, nil - // The account doesn't exist. If we're recovering accounts, - // that's fine. We just skip this account key and try the next - // one. If we're not in recovery mode, this is a hard failure. - case *clmrpc.ServerAuctionMessage_Error: + // We got an error. If we're in recovery mode, this could either + // mean the account doesn't exist or there's only a reservation + // for it. + case srvMsg.GetError() != nil: + errMsg := srvMsg.GetError() + + // If we're not in recovery mode we don't expect any + // error, this is a hard failure. if !recovery { return nil, false, fmt.Errorf("error "+ "subscribing to account: %v", - msg.Error.Error) + errMsg.Error) } - return sub, false, nil + + // If we're in recovery mode, it is possible for an + // account to still be in the reservation phase from the + // point of view of the auctioneer. We cannot do the + // normal recovery in that case because the account + // subscription cannot be completed. This needs to be + // handled specifically so we return a typed error now. + if errMsg.ErrorCode == clmrpc.SubscribeError_INCOMPLETE_ACCOUNT_RESERVATION { + return sub, true, AcctResNotCompletedErrFromRPC( + *errMsg.AccountReservation, + ) + } + + // The account doesn't exist. We are in recovery mode so + // this is fine. We just skip this account key and try + // the next one. + if errMsg.ErrorCode == clmrpc.SubscribeError_ACCOUNT_DOES_NOT_EXIST { + return sub, false, nil + } + + return nil, false, fmt.Errorf("unknown error message "+ + "received: %v", errMsg) default: return nil, false, fmt.Errorf("unknown message "+ - "received: %v", msg) + "received: %v", srvMsg) } case <-c.quit: @@ -530,14 +555,44 @@ func (c *Client) RecoverAccounts(ctx context.Context, var recoveredAccounts []*account.Account for _, keyDesc := range accountKeys { acctKeyBytes := keyDesc.PubKey.SerializeCompressed() + var resErr *AcctResNotCompletedError + + // Start the recovery of this account by connecting and going + // through the 3-way-handshake. There's four possibilities of + // what can happen: 1. The account exists but only as a + // reservation. This special case needs to be handled + // separately. 2. We get an unexpected error in which case we + // abort the recovery. 3. The account doesn't exist and we skip + // it. 4. The account exists and we get a success message. + // Normal recovery can be attempted. subscription, canRecover, err := c.connectAndAuthenticate( ctx, keyDesc, true, ) - if err != nil { - return nil, fmt.Errorf("could not recover account %x: %v", - acctKeyBytes, err) + + // Case 1: Special recovery of an account that was only + // reserved. + if errors.As(err, &resErr) { + acct, err := incompleteAcctFromErr(keyDesc, resErr) + if err != nil { + return nil, fmt.Errorf("could not recover "+ + "reserved account %x: %v", acctKeyBytes, + err) + } + recoveredAccounts = append(recoveredAccounts, acct) + + // Don't send any further messages to the auctioneer, + // we won't get any more information than was in the + // error. + continue } + // Case 2: Abort on unexpected error. + if err != nil { + return nil, fmt.Errorf("could not recover account %x: "+ + "%v", acctKeyBytes, err) + } + + // Case 3: The auctioneer doesn't know the account. if !canRecover { numNotFoundAccounts++ @@ -550,10 +605,11 @@ func (c *Client) RecoverAccounts(ctx context.Context, continue } - // Reset our not found counter as we've found a recoverable - // account here. Then ask the auctioneer to send us back the - // state as it knows it in its database. The response to this - // message will be handled outside of the client. + // Case 4: Attempt normal recovery. First we reset our not found + // counter as we've found a recoverable account here. Then ask + // the auctioneer to send us back the state as it knows it in + // its database. The response to this message will be handled + // outside of the client. numNotFoundAccounts = 0 err = c.SendAuctionMessage(&clmrpc.ClientAuctionMessage{ Msg: &clmrpc.ClientAuctionMessage_Recover{ @@ -594,6 +650,39 @@ func (c *Client) RecoverAccounts(ctx context.Context, return recoveredAccounts, nil } +// incompleteAcctFromErr creates an account in the state initiated from an error +// that contains the bare minimum of recovery information. +func incompleteAcctFromErr(traderKey *keychain.KeyDescriptor, + resErr *AcctResNotCompletedError) (*account.Account, error) { + + var ( + acct = &account.Account{ + Value: resErr.Value, + State: account.StateInitiated, + TraderKey: traderKey, + Expiry: resErr.Expiry, + HeightHint: resErr.HeightHint, + } + err error + ) + + acct.AuctioneerKey, err = btcec.ParsePubKey( + resErr.AuctioneerKey[:], btcec.S256(), + ) + if err != nil { + return nil, fmt.Errorf("error parsing auctioneer key: %v", err) + } + + acct.BatchKey, err = btcec.ParsePubKey( + resErr.InitialBatchKey[:], btcec.S256(), + ) + if err != nil { + return nil, fmt.Errorf("error parsing batch key: %v", err) + } + + return acct, nil +} + // SendAuctionMessage sends an auction message through the long-lived stream to // the auction server. A message can only be sent as a response to a server // message, therefore the stream must already be open. @@ -784,9 +873,10 @@ func (c *Client) readIncomingStream() { // nolint:gocyclo case *clmrpc.ServerAuctionMessage_Error: errCode := t.Error.ErrorCode + switch errCode { // The server is shutting down. No need to forward this, // we can just shutdown the stream and try to reconnect. - if errCode == clmrpc.SubscribeError_SERVER_SHUTDOWN { + case clmrpc.SubscribeError_SERVER_SHUTDOWN: err := c.HandleServerShutdown(nil) if err != nil { select { @@ -795,12 +885,13 @@ func (c *Client) readIncomingStream() { // nolint:gocyclo } } return - } // We received an account not found error. This is not // a reason to abort in case we are in recovery mode. // We let the subscription decide what to do. - if errCode == clmrpc.SubscribeError_ACCOUNT_DOES_NOT_EXIST { + case clmrpc.SubscribeError_ACCOUNT_DOES_NOT_EXIST, + clmrpc.SubscribeError_INCOMPLETE_ACCOUNT_RESERVATION: + err := c.sendToSubscription( t.Error.TraderKey, msg, )