Skip to content

Commit bc92117

Browse files
committed
exclude Actor.PrivateKey from GORM, read via raw SQL in export-keys
1 parent fb7b9cc commit bc92117

4 files changed

Lines changed: 197 additions & 104 deletions

File tree

handler/wallet/export_keys.go

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,14 @@ type ExportKeysResult struct {
1717
Errors []string // actors that failed (logged but don't abort)
1818
}
1919

20+
// actor row with the legacy private_key column, used only by export-keys.
21+
// Actor.PrivateKey is gorm:"-" in the model so we need a local type for raw SQL.
22+
type legacyActorRow struct {
23+
ID string
24+
Address string
25+
PrivateKey string
26+
}
27+
2028
// exports private keys from the legacy Actor.PrivateKey column into the
2129
// filesystem keystore, creating Wallet records where needed.
2230
// idempotent -- skips actors whose address already has a Wallet record.
@@ -28,8 +36,8 @@ func ExportKeysHandler(
2836
) (*ExportKeysResult, error) {
2937
db = db.WithContext(ctx)
3038

31-
var actors []model.Actor
32-
err := db.Where("private_key != '' AND private_key IS NOT NULL").Find(&actors).Error
39+
var actors []legacyActorRow
40+
err := db.Raw("SELECT id, address, private_key FROM actors WHERE private_key != '' AND private_key IS NOT NULL").Scan(&actors).Error
3341
if err != nil {
3442
return nil, errors.Wrap(err, "failed to query actors with private keys")
3543
}
@@ -49,12 +57,59 @@ func ExportKeysHandler(
4957
}
5058
}
5159

60+
// after exporting keys, migrate wallet_assignments if the legacy table exists
61+
if err := migrateWalletAssignments(db); err != nil {
62+
result.Errors = append(result.Errors, fmt.Sprintf("wallet_assignments migration: %v", err))
63+
}
64+
5265
return result, nil
5366
}
5467

68+
// migrates legacy wallet_assignments (preparation_id, wallet_id=actor_id_string)
69+
// to preparation.wallet_id (uint FK to new wallets table). drops the table after.
70+
func migrateWalletAssignments(db *gorm.DB) error {
71+
if !db.Migrator().HasTable("wallet_assignments") {
72+
return nil
73+
}
74+
75+
type row struct {
76+
PreparationID uint
77+
WalletID string // old actor ID (f0...)
78+
}
79+
var rows []row
80+
if err := db.Raw("SELECT preparation_id, wallet_id FROM wallet_assignments").Scan(&rows).Error; err != nil {
81+
return errors.Wrap(err, "failed to read wallet_assignments")
82+
}
83+
84+
migrated := 0
85+
for _, r := range rows {
86+
// find the new wallet by actor_id
87+
var wallet model.Wallet
88+
if err := db.Where("actor_id = ?", r.WalletID).First(&wallet).Error; err != nil {
89+
logger.Warnw("wallet_assignment: no wallet found for actor, skipping",
90+
"preparation_id", r.PreparationID, "actor_id", r.WalletID)
91+
continue
92+
}
93+
if err := db.Exec("UPDATE preparations SET wallet_id = ? WHERE id = ? AND wallet_id IS NULL",
94+
wallet.ID, r.PreparationID).Error; err != nil {
95+
return errors.Wrapf(err, "failed to set wallet_id for preparation %d", r.PreparationID)
96+
}
97+
migrated++
98+
}
99+
100+
if err := db.Migrator().DropTable("wallet_assignments"); err != nil {
101+
return errors.Wrap(err, "failed to drop wallet_assignments")
102+
}
103+
104+
if migrated > 0 {
105+
logger.Infow("migrated wallet_assignments to preparation.wallet_id", "migrated", migrated)
106+
}
107+
return nil
108+
}
109+
55110
// exports a single actor's key to keystore, returns (true, "") on success,
56111
// (false, "") on skip, ("", errMsg) on failure
57-
func exportOneKey(db *gorm.DB, ks keystore.KeyStore, actor model.Actor) (exported bool, errMsg string) {
112+
func exportOneKey(db *gorm.DB, ks keystore.KeyStore, actor legacyActorRow) (exported bool, errMsg string) {
58113
// derive address to check for existing wallet
59114
addr, err := keystore.AddressFromExport(actor.PrivateKey)
60115
if err != nil {
@@ -114,7 +169,17 @@ func exportOneKey(db *gorm.DB, ks keystore.KeyStore, actor model.Actor) (exporte
114169
}
115170

116171
func HasPrivateKeyColumn(db *gorm.DB) bool {
117-
return db.Migrator().HasColumn(&model.Actor{}, "private_key")
172+
// can't use db.Migrator().HasColumn(&model.Actor{}, "private_key") because
173+
// the field is gorm:"-" -- GORM won't resolve the column name. query directly.
174+
dialect := db.Dialector.Name()
175+
var count int64
176+
switch dialect {
177+
case "sqlite":
178+
db.Raw("SELECT COUNT(*) FROM pragma_table_info('actors') WHERE name = 'private_key'").Scan(&count)
179+
default:
180+
db.Raw("SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = current_schema() AND table_name = 'actors' AND column_name = 'private_key'").Scan(&count)
181+
}
182+
return count > 0
118183
}
119184

120185
// counts actors that still have a non-empty private_key in the database.

handler/wallet/export_keys_test.go

Lines changed: 56 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,38 @@ import (
1212
"gorm.io/gorm"
1313
)
1414

15+
// model without -:migration, used to simulate a legacy database where
16+
// AutoMigrate created the private_key column
17+
type legacyActor struct {
18+
ID string `gorm:"primaryKey;size:15"`
19+
Address string `gorm:"index"`
20+
PrivateKey string
21+
}
22+
23+
func (legacyActor) TableName() string { return "actors" }
24+
25+
// simulates a legacy database by running AutoMigrate with the old model
26+
// that includes private_key as a normal column
27+
func addLegacyColumn(t *testing.T, db *gorm.DB) {
28+
t.Helper()
29+
require.NoError(t, db.AutoMigrate(&legacyActor{}))
30+
}
31+
32+
// inserts an actor with a legacy private_key via the legacy model
33+
func createLegacyActor(t *testing.T, db *gorm.DB, id, address, privateKey string) {
34+
t.Helper()
35+
require.NoError(t, db.Create(&legacyActor{
36+
ID: id, Address: address, PrivateKey: privateKey,
37+
}).Error)
38+
}
39+
1540
func TestExportKeysHandler_ExportsActorKey(t *testing.T) {
1641
testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) {
1742
ks, err := keystore.NewLocalKeyStore(t.TempDir())
1843
require.NoError(t, err)
1944

20-
// create an actor with a legacy private key
21-
actor := model.Actor{
22-
ID: "f01234",
23-
Address: testutil.TestWalletAddr,
24-
PrivateKey: testutil.TestPrivateKeyHex,
25-
}
26-
require.NoError(t, db.Create(&actor).Error)
45+
addLegacyColumn(t, db)
46+
createLegacyActor(t, db, "f01234", testutil.TestWalletAddr, testutil.TestPrivateKeyHex)
2747

2848
result, err := ExportKeysHandler(ctx, db, ks)
2949
require.NoError(t, err)
@@ -50,13 +70,8 @@ func TestExportKeysHandler_SkipsExistingWallet(t *testing.T) {
5070
ks, err := keystore.NewLocalKeyStore(t.TempDir())
5171
require.NoError(t, err)
5272

53-
// create actor with legacy key
54-
actor := model.Actor{
55-
ID: "f01234",
56-
Address: testutil.TestWalletAddr,
57-
PrivateKey: testutil.TestPrivateKeyHex,
58-
}
59-
require.NoError(t, db.Create(&actor).Error)
73+
addLegacyColumn(t, db)
74+
createLegacyActor(t, db, "f01234", testutil.TestWalletAddr, testutil.TestPrivateKeyHex)
6075

6176
// pre-import the wallet via normal import path
6277
h := DefaultHandler{}
@@ -83,13 +98,8 @@ func TestExportKeysHandler_SkipsExistingWalletLinksActor(t *testing.T) {
8398
ks, err := keystore.NewLocalKeyStore(t.TempDir())
8499
require.NoError(t, err)
85100

86-
// create actor with legacy key
87-
actor := model.Actor{
88-
ID: "f01234",
89-
Address: testutil.TestWalletAddr,
90-
PrivateKey: testutil.TestPrivateKeyHex,
91-
}
92-
require.NoError(t, db.Create(&actor).Error)
101+
addLegacyColumn(t, db)
102+
createLegacyActor(t, db, "f01234", testutil.TestWalletAddr, testutil.TestPrivateKeyHex)
93103

94104
// pre-import the wallet WITHOUT actor linkage
95105
h := DefaultHandler{}
@@ -116,12 +126,8 @@ func TestExportKeysHandler_Idempotent(t *testing.T) {
116126
ks, err := keystore.NewLocalKeyStore(t.TempDir())
117127
require.NoError(t, err)
118128

119-
actor := model.Actor{
120-
ID: "f01234",
121-
Address: testutil.TestWalletAddr,
122-
PrivateKey: testutil.TestPrivateKeyHex,
123-
}
124-
require.NoError(t, db.Create(&actor).Error)
129+
addLegacyColumn(t, db)
130+
createLegacyActor(t, db, "f01234", testutil.TestWalletAddr, testutil.TestPrivateKeyHex)
125131

126132
// first run exports
127133
r1, err := ExportKeysHandler(ctx, db, ks)
@@ -146,12 +152,12 @@ func TestExportKeysHandler_NoActorsWithKeys(t *testing.T) {
146152
ks, err := keystore.NewLocalKeyStore(t.TempDir())
147153
require.NoError(t, err)
148154

155+
addLegacyColumn(t, db)
149156
// actor with no private key
150-
actor := model.Actor{
157+
require.NoError(t, db.Create(&model.Actor{
151158
ID: "f09999",
152159
Address: "f1abc",
153-
}
154-
require.NoError(t, db.Create(&actor).Error)
160+
}).Error)
155161

156162
result, err := ExportKeysHandler(ctx, db, ks)
157163
require.NoError(t, err)
@@ -166,13 +172,8 @@ func TestExportKeysHandler_InvalidKeyRecordsError(t *testing.T) {
166172
ks, err := keystore.NewLocalKeyStore(t.TempDir())
167173
require.NoError(t, err)
168174

169-
// actor with garbage key
170-
actor := model.Actor{
171-
ID: "f05555",
172-
Address: "f1bad",
173-
PrivateKey: "not-a-valid-key",
174-
}
175-
require.NoError(t, db.Create(&actor).Error)
175+
addLegacyColumn(t, db)
176+
createLegacyActor(t, db, "f05555", "f1bad", "not-a-valid-key")
176177

177178
result, err := ExportKeysHandler(ctx, db, ks)
178179
require.NoError(t, err) // overall operation succeeds
@@ -189,13 +190,8 @@ func TestExportKeysHandler_MissingKeyFile(t *testing.T) {
189190
ks, err := keystore.NewLocalKeyStore(t.TempDir())
190191
require.NoError(t, err)
191192

192-
// create actor with legacy key
193-
actor := model.Actor{
194-
ID: "f01234",
195-
Address: testutil.TestWalletAddr,
196-
PrivateKey: testutil.TestPrivateKeyHex,
197-
}
198-
require.NoError(t, db.Create(&actor).Error)
193+
addLegacyColumn(t, db)
194+
createLegacyActor(t, db, "f01234", testutil.TestWalletAddr, testutil.TestPrivateKeyHex)
199195

200196
// create wallet record pointing to a nonexistent key file
201197
require.NoError(t, db.Create(&model.Wallet{
@@ -219,12 +215,8 @@ func TestExportKeysHandler_CorruptKeyFile(t *testing.T) {
219215
ks, err := keystore.NewLocalKeyStore(dir)
220216
require.NoError(t, err)
221217

222-
actor := model.Actor{
223-
ID: "f01234",
224-
Address: testutil.TestWalletAddr,
225-
PrivateKey: testutil.TestPrivateKeyHex,
226-
}
227-
require.NoError(t, db.Create(&actor).Error)
218+
addLegacyColumn(t, db)
219+
createLegacyActor(t, db, "f01234", testutil.TestWalletAddr, testutil.TestPrivateKeyHex)
228220

229221
// write garbage to the key file path
230222
corruptPath := dir + "/corrupt"
@@ -245,17 +237,22 @@ func TestExportKeysHandler_CorruptKeyFile(t *testing.T) {
245237
})
246238
}
247239

248-
func TestDropPrivateKeyColumn(t *testing.T) {
240+
func TestExportKeysHandler_AutoMigrateDoesNotRecreateColumn(t *testing.T) {
249241
testutil.All(t, func(ctx context.Context, t *testing.T, db *gorm.DB) {
250-
// column exists after AutoMigrate
251-
require.True(t, db.Migrator().HasColumn(&model.Actor{}, "private_key"))
242+
// AutoMigrate already ran (via testutil.All) -- column should NOT exist
243+
require.False(t, db.Migrator().HasColumn(&model.Actor{}, "private_key"),
244+
"AutoMigrate must not create private_key column (gorm:-:migration)")
252245

253-
// drop it
246+
// simulate legacy db, export, drop
247+
addLegacyColumn(t, db)
248+
require.True(t, HasPrivateKeyColumn(db))
254249
require.NoError(t, DropPrivateKeyColumn(db))
255-
require.False(t, db.Migrator().HasColumn(&model.Actor{}, "private_key"))
250+
require.False(t, HasPrivateKeyColumn(db))
256251

257-
// idempotent -- second call is a no-op
258-
require.NoError(t, DropPrivateKeyColumn(db))
252+
// re-run AutoMigrate -- must NOT re-add the column
253+
require.NoError(t, model.AutoMigrate(db))
254+
require.False(t, HasPrivateKeyColumn(db),
255+
"AutoMigrate must not re-add private_key column after drop")
259256
})
260257
}
261258

@@ -264,13 +261,8 @@ func TestDropPrivateKeyColumn_ExportThenDrop(t *testing.T) {
264261
ks, err := keystore.NewLocalKeyStore(t.TempDir())
265262
require.NoError(t, err)
266263

267-
// create actor with legacy key
268-
actor := model.Actor{
269-
ID: "f01234",
270-
Address: testutil.TestWalletAddr,
271-
PrivateKey: testutil.TestPrivateKeyHex,
272-
}
273-
require.NoError(t, db.Create(&actor).Error)
264+
addLegacyColumn(t, db)
265+
createLegacyActor(t, db, "f01234", testutil.TestWalletAddr, testutil.TestPrivateKeyHex)
274266

275267
// export
276268
result, err := ExportKeysHandler(ctx, db, ks)

0 commit comments

Comments
 (0)