Skip to content

Commit 17ba66f

Browse files
committed
assets+loopdb: implement asset deposit withdrawal
This commit implements the necessary plumbing to enable deposit withdrawal. Withdrawing reveals the server's internal key for the deposit, allowing the client to sweep it independently.
1 parent f0ed1b6 commit 17ba66f

File tree

8 files changed

+148
-2
lines changed

8 files changed

+148
-2
lines changed

assets/deposit/manager.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1053,3 +1053,76 @@ func (m *Manager) releaseDepositSweepInputs(ctx context.Context,
10531053

10541054
return nil
10551055
}
1056+
1057+
// WithdrawDeposits withdraws the deposits with the given IDs. It will first ask
1058+
// the server for the deposit keys, then initate the withdrawal by updating the
1059+
// deposit state.
1060+
func (m *Manager) WithdrawDeposits(ctx context.Context,
1061+
depositIDs []string) error {
1062+
1063+
done, err := m.scheduleNextCall()
1064+
if err != nil {
1065+
return err
1066+
}
1067+
defer done()
1068+
1069+
for _, depositID := range depositIDs {
1070+
d, ok := m.deposits[depositID]
1071+
if !ok {
1072+
return fmt.Errorf("deposit %v not found", depositID)
1073+
}
1074+
1075+
if d.State != StateConfirmed {
1076+
return fmt.Errorf("deposit %v is not withdrawable, "+
1077+
"current state: %v", depositID, d.State)
1078+
}
1079+
1080+
log.Infof("Initiating deposit withdrawal %v: %v",
1081+
depositID, d.Amount)
1082+
}
1083+
1084+
keys, err := m.depositServiceClient.WithdrawAssetDeposits(
1085+
ctx, &swapserverrpc.WithdrawAssetDepositsServerReq{
1086+
DepositIds: depositIDs,
1087+
},
1088+
)
1089+
if err != nil {
1090+
return fmt.Errorf("unable to request withdrawal: %w", err)
1091+
}
1092+
1093+
for depositID, privKeyBytes := range keys.DepositKeys {
1094+
d, ok := m.deposits[depositID]
1095+
if !ok {
1096+
log.Warnf("Skipping withdrawal of unknown deposit: %v",
1097+
depositID)
1098+
continue
1099+
}
1100+
1101+
privKey, pubKey := btcec.PrivKeyFromBytes(privKeyBytes)
1102+
if !d.CoSignerInternalKey.IsEqual(pubKey) {
1103+
return fmt.Errorf("revealed co-signer internal key "+
1104+
"does not match local key for %v", depositID)
1105+
}
1106+
1107+
err := m.store.SetAssetDepositServerKey(ctx, depositID, privKey)
1108+
if err != nil {
1109+
return err
1110+
}
1111+
1112+
d.State = StateWithdrawn
1113+
err = d.GenerateSweepKeys(ctx, m.tapClient)
1114+
if err != nil {
1115+
log.Errorf("Unable to generate sweep keys for deposit "+
1116+
"withdrawal %v: %v", d.ID, err)
1117+
1118+
return err
1119+
}
1120+
1121+
err = m.handleDepositStateUpdate(ctx, d)
1122+
if err != nil {
1123+
return err
1124+
}
1125+
}
1126+
1127+
return nil
1128+
}

assets/deposit/manager_test.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"testing"
77
"time"
88

9+
"github.com/btcsuite/btcd/btcec/v2"
910
"github.com/btcsuite/btcd/chaincfg"
1011
"github.com/btcsuite/btcd/chaincfg/chainhash"
1112
"github.com/btcsuite/btcd/wire"
@@ -45,6 +46,14 @@ func (s *mockStore) GetActiveDeposits(context.Context) ([]Deposit, error) {
4546
return []Deposit{}, nil
4647
}
4748

49+
// SetAssetDepositServerKey is a mock implementation of the
50+
// SetAssetDepositServerKey method.
51+
func (s *mockStore) SetAssetDepositServerKey(context.Context, string,
52+
*btcec.PrivateKey) error {
53+
54+
return nil
55+
}
56+
4857
// testAddDeposit is a helper function that (intrusively) adds a deposit to the
4958
// manager.
5059
func testAddDeposit(t *testing.T, m *Manager, d *Deposit) {

assets/deposit/server.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,5 +149,19 @@ func (s *Server) WithdrawAssetDeposits(ctx context.Context,
149149
in *looprpc.WithdrawAssetDepositsRequest) (
150150
*looprpc.WithdrawAssetDepositsResponse, error) {
151151

152-
return nil, status.Error(codes.Unimplemented, "unimplemented")
152+
if s.manager == nil {
153+
return nil, ErrAssetDepositsUnavailable
154+
}
155+
156+
if len(in.DepositIds) == 0 {
157+
return nil, status.Error(codes.InvalidArgument,
158+
"at least one deposit id must be provided")
159+
}
160+
161+
err := s.manager.WithdrawDeposits(ctx, in.DepositIds)
162+
if err != nil {
163+
return nil, status.Error(codes.Internal, err.Error())
164+
}
165+
166+
return &looprpc.WithdrawAssetDepositsResponse{}, nil
153167
}

assets/deposit/sql_store.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ type Querier interface {
3535

3636
GetActiveAssetDeposits(ctx context.Context) (
3737
[]sqlc.GetActiveAssetDepositsRow, error)
38+
39+
SetAssetDepositServerInternalKey(ctx context.Context,
40+
arg sqlc.SetAssetDepositServerInternalKeyParams) error
3841
}
3942

4043
// DepositBaseDB is the interface that contains all the queries generated
@@ -135,6 +138,8 @@ func (s *SQLStore) UpdateDeposit(ctx context.Context, d *Deposit) error {
135138
}
136139

137140
case StateExpired:
141+
fallthrough
142+
case StateWithdrawn:
138143
scriptKey := d.SweepScriptKey.SerializeCompressed()
139144
internalKey := d.SweepInternalKey.SerializeCompressed()
140145
err := tx.SetAssetDepositSweepKeys(
@@ -311,3 +316,16 @@ func (s *SQLStore) GetActiveDeposits(ctx context.Context) ([]Deposit, error) {
311316

312317
return deposits, nil
313318
}
319+
320+
// SetAssetDepositServerKey sets the server's internal key for the give asset
321+
// deposit.
322+
func (s *SQLStore) SetAssetDepositServerKey(ctx context.Context,
323+
depositID string, key *btcec.PrivateKey) error {
324+
325+
return s.db.SetAssetDepositServerInternalKey(
326+
ctx, sqlc.SetAssetDepositServerInternalKeyParams{
327+
DepositID: depositID,
328+
ServerInternalKey: key.Serialize(),
329+
},
330+
)
331+
}

assets/deposit/store.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
package deposit
22

3-
import "context"
3+
import (
4+
"context"
5+
6+
"github.com/btcsuite/btcd/btcec/v2"
7+
)
48

59
// Store defines the interface that the Manager requires from the storage layer.
610
type Store interface {
@@ -17,4 +21,9 @@ type Store interface {
1721
// GetActiveDeposits returns all active deposits from the database.
1822
// Active deposits are those that have not yet been spent or swept.
1923
GetActiveDeposits(ctx context.Context) ([]Deposit, error)
24+
25+
// SetAssetDepositServerKey sets the server's internal key for the given
26+
// asset deposit.
27+
SetAssetDepositServerKey(ctx context.Context, depositID string,
28+
key *btcec.PrivateKey) error
2029
}

loopdb/sqlc/asset_deposits.sql.go

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

loopdb/sqlc/querier.go

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

loopdb/sqlc/queries/asset_deposits.sql

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,8 @@ WHERE u.id = (
5959
)
6060
AND u.update_state IN (0, 1, 2, 3, 4, 5, 6);
6161

62+
-- name: SetAssetDepositServerInternalKey :exec
63+
UPDATE asset_deposits
64+
SET server_internal_key = $2
65+
WHERE deposit_id = $1
66+
AND server_internal_key IS NULL;

0 commit comments

Comments
 (0)