Skip to content

Commit d4c9702

Browse files
authored
feat(SPV-1387, SPV-1396): replace estimated unlocking script size with estimated input size; rename UsersUTXO (#857)
1 parent b06ac5a commit d4c9702

13 files changed

Lines changed: 98 additions & 89 deletions

File tree

engine/database/models.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ func Models() []any {
99
User{},
1010
Paymail{},
1111
Address{},
12-
UserUtxos{},
12+
UserUTXO{},
1313
Operation{},
1414
}
1515
}

engine/database/repository/users.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ func (u *Users) GetBalance(ctx context.Context, userID string, bucket string) (b
5050
var balance bsv.Satoshis
5151
err := u.db.
5252
WithContext(ctx).
53-
Model(&database.UserUtxos{}).
53+
Model(&database.UserUTXO{}).
5454
Where("user_id = ? AND bucket = ?", userID, bucket).
5555
Select("COALESCE(SUM(satoshis), 0)").
5656
Row().

engine/database/testabilities/fixture_database.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ type UserUtxoFixture interface {
2626
// WithSatoshis sets the satoshis value of the UTXO.
2727
WithSatoshis(satoshis bsv.Satoshis) UserUtxoFixture
2828

29-
Storable[database.UserUtxos]
29+
Storable[database.UserUTXO]
3030
}
3131

3232
type Storable[Data any] interface {

engine/database/testabilities/user_utxo_fixture.go

Lines changed: 28 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -8,32 +8,33 @@ import (
88
"github.com/bitcoin-sv/spv-wallet/engine/database"
99
"github.com/bitcoin-sv/spv-wallet/engine/tester/fixtures"
1010
"github.com/bitcoin-sv/spv-wallet/models/bsv"
11+
"github.com/bitcoin-sv/spv-wallet/models/transaction/bucket"
1112
"gorm.io/gorm"
1213
)
1314

1415
var FirstCreatedAt = time.Date(2006, 02, 01, 15, 4, 5, 7, time.UTC)
1516

1617
type userUtxoFixture struct {
17-
db *gorm.DB
18-
t testing.TB
19-
index uint
20-
userID string
21-
txID string
22-
vout uint32
23-
satoshis bsv.Satoshis
24-
unlockingScriptEstimatedSize uint64
18+
db *gorm.DB
19+
t testing.TB
20+
index uint
21+
userID string
22+
txID string
23+
vout uint32
24+
satoshis bsv.Satoshis
25+
estimatedInputSize uint64
2526
}
2627

2728
func newUtxoFixture(t testing.TB, db *gorm.DB, index uint32) *userUtxoFixture {
2829
return &userUtxoFixture{
29-
t: t,
30-
db: db,
31-
index: uint(index),
32-
userID: fixtures.Sender.ID(),
33-
txID: txIDTemplated(uint(index)),
34-
vout: index,
35-
satoshis: 1,
36-
unlockingScriptEstimatedSize: 106,
30+
t: t,
31+
db: db,
32+
index: uint(index),
33+
userID: fixtures.Sender.ID(),
34+
txID: txIDTemplated(uint(index)),
35+
vout: index,
36+
satoshis: 1,
37+
estimatedInputSize: database.EstimatedInputSizeForP2PKH,
3738
}
3839
}
3940

@@ -47,7 +48,7 @@ func (f *userUtxoFixture) OwnedBySender() UserUtxoFixture {
4748
}
4849

4950
func (f *userUtxoFixture) P2PKH() UserUtxoFixture {
50-
f.unlockingScriptEstimatedSize = fixtures.EstimatedUnlockingScriptSizeForP2PKH
51+
f.estimatedInputSize = database.EstimatedInputSizeForP2PKH
5152
return f
5253
}
5354

@@ -56,16 +57,16 @@ func (f *userUtxoFixture) WithSatoshis(satoshis bsv.Satoshis) UserUtxoFixture {
5657
return f
5758
}
5859

59-
func (f *userUtxoFixture) Stored() *database.UserUtxos {
60-
utxo := &database.UserUtxos{
61-
UserID: f.userID,
62-
TxID: f.txID,
63-
Vout: f.vout,
64-
Satoshis: uint64(f.satoshis),
65-
UnlockingScriptEstimatedSize: f.unlockingScriptEstimatedSize,
66-
Bucket: "bsv",
67-
CreatedAt: FirstCreatedAt.Add(time.Duration(f.index) * time.Second), //nolint:gosec // this is used for testing and it should be fine even in case of integer overflow.
68-
TouchedAt: FirstCreatedAt.Add(time.Duration(24) * time.Hour),
60+
func (f *userUtxoFixture) Stored() *database.UserUTXO {
61+
utxo := &database.UserUTXO{
62+
UserID: f.userID,
63+
TxID: f.txID,
64+
Vout: f.vout,
65+
Satoshis: uint64(f.satoshis),
66+
EstimatedInputSize: f.estimatedInputSize,
67+
Bucket: string(bucket.BSV),
68+
CreatedAt: FirstCreatedAt.Add(time.Duration(f.index) * time.Second), //nolint:gosec // this is used for testing and it should be fine even in case of integer overflow.
69+
TouchedAt: FirstCreatedAt.Add(time.Duration(24) * time.Hour),
6970
}
7071

7172
f.db.Create(utxo)

engine/database/tracked_transaction.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ type TrackedTransaction struct {
2222
Inputs []*TrackedOutput `gorm:"foreignKey:SpendingTX"`
2323
Outputs []*TrackedOutput `gorm:"foreignKey:TxID"`
2424

25-
newUTXOs []*UserUtxos `gorm:"-"`
25+
newUTXOs []*UserUTXO `gorm:"-"`
2626
}
2727

2828
// CreateP2PKHOutput prepares a new P2PKH output and adds it to the transaction.
@@ -52,7 +52,7 @@ func (t *TrackedTransaction) AddInputs(inputs ...*TrackedOutput) {
5252
func (t *TrackedTransaction) AfterCreate(tx *gorm.DB) error {
5353
// Add new UTXOs
5454
if len(t.newUTXOs) > 0 {
55-
err := tx.Model(&UserUtxos{}).Create(t.newUTXOs).Error
55+
err := tx.Model(&UserUTXO{}).Create(t.newUTXOs).Error
5656
if err != nil {
5757
return spverrors.Wrapf(err, "failed to save user utxos")
5858
}
@@ -67,7 +67,7 @@ func (t *TrackedTransaction) AfterCreate(tx *gorm.DB) error {
6767
}
6868
})
6969
if len(spentOutpoints) > 0 {
70-
err := tx.Where("(tx_id, vout) IN ?", spentOutpoints).Delete(&UserUtxos{}).Error
70+
err := tx.Where("(tx_id, vout) IN ?", spentOutpoints).Delete(&UserUTXO{}).Error
7171
if err != nil {
7272
return spverrors.Wrapf(err, "failed to delete spent utxos")
7373
}

engine/database/user_utxos.go

Lines changed: 32 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,29 +6,39 @@ import (
66
"gorm.io/datatypes"
77
)
88

9-
// UserUtxos is a table holding user's Unspent Transaction Outputs (UTXOs).
10-
// TODO: It should be renamed to UserUTXO.
11-
type UserUtxos struct {
12-
UserID string `gorm:"primaryKey;uniqueIndex:idx_window,sort:asc,priority:1"`
13-
TxID string `gorm:"primaryKey;uniqueIndex:idx_window,sort:asc,priority:4"`
14-
Vout uint32 `gorm:"primaryKey;uniqueIndex:idx_window,sort:asc,priority:5"`
15-
Satoshis uint64
16-
UnlockingScriptEstimatedSize uint64
17-
Bucket string `gorm:"check:chk_not_data_bucket,bucket <> 'data'"`
18-
CreatedAt time.Time `gorm:"uniqueIndex:idx_window,sort:asc,priority:3"`
19-
TouchedAt time.Time `gorm:"uniqueIndex:idx_window,sort:asc,priority:2"`
20-
CustomInstructions datatypes.JSONSlice[CustomInstruction]
9+
// EstimatedInputSizeForP2PKH is the estimated size increase when adding and unlocking P2PKH input to transaction.
10+
// 32 bytes txID
11+
// + 4 bytes vout index
12+
// + 1 byte script length
13+
// + 107 bytes script pub key
14+
// + 4 bytes nSequence
15+
const EstimatedInputSizeForP2PKH = 148
16+
17+
// UserUTXO is a table holding user's Unspent Transaction Outputs (UTXOs).
18+
type UserUTXO struct {
19+
UserID string `gorm:"primaryKey;uniqueIndex:idx_window,sort:asc,priority:1"`
20+
TxID string `gorm:"primaryKey;uniqueIndex:idx_window,sort:asc,priority:4"`
21+
Vout uint32 `gorm:"primaryKey;uniqueIndex:idx_window,sort:asc,priority:5"`
22+
Satoshis uint64
23+
// EstimatedInputSize is the estimated size increase when adding and unlocking this UTXO to a transaction.
24+
EstimatedInputSize uint64
25+
Bucket string `gorm:"check:chk_not_data_bucket,bucket <> 'data'"`
26+
CreatedAt time.Time `gorm:"uniqueIndex:idx_window,sort:asc,priority:3"`
27+
// TouchedAt is the time when the UTXO was last touched (selected for preparing transaction outline) - used for prioritizing UTXO selection.
28+
TouchedAt time.Time `gorm:"uniqueIndex:idx_window,sort:asc,priority:2"`
29+
// CustomInstructions is the list of instructions for unlocking given UTXO (it should be understood by client).
30+
CustomInstructions datatypes.JSONSlice[CustomInstruction]
2131
}
2232

23-
// NewP2PKHUserUTXO creates a new UserUtxos instance for a P2PKH output based on the given output and custom instructions.
24-
func NewP2PKHUserUTXO(output *TrackedOutput, customInstructions datatypes.JSONSlice[CustomInstruction]) *UserUtxos {
25-
return &UserUtxos{
26-
UserID: output.UserID,
27-
TxID: output.TxID,
28-
Vout: output.Vout,
29-
Satoshis: uint64(output.Satoshis),
30-
UnlockingScriptEstimatedSize: 106,
31-
Bucket: "bsv",
32-
CustomInstructions: customInstructions,
33+
// NewP2PKHUserUTXO creates a new UserUTXO instance for a P2PKH output based on the given output and custom instructions.
34+
func NewP2PKHUserUTXO(output *TrackedOutput, customInstructions datatypes.JSONSlice[CustomInstruction]) *UserUTXO {
35+
return &UserUTXO{
36+
UserID: output.UserID,
37+
TxID: output.TxID,
38+
Vout: output.Vout,
39+
Satoshis: uint64(output.Satoshis),
40+
EstimatedInputSize: EstimatedInputSizeForP2PKH,
41+
Bucket: "bsv",
42+
CustomInstructions: customInstructions,
3343
}
3444
}

engine/tester/fixtures/tx_const_fixtures.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,3 @@ var DefaultFeeUnit = bsv.FeeUnit{
77
Satoshis: 1,
88
Bytes: 1000,
99
}
10-
11-
// EstimatedUnlockingScriptSizeForP2PKH is the estimated unlocking script size for a P2PKH transaction.
12-
const EstimatedUnlockingScriptSizeForP2PKH = 106

engine/tester/fixtures/tx_fixtures_test.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ func TestMockTXGeneration(t *testing.T) {
4949
spec := test.spec
5050

5151
// when
52-
ok, err := spv.VerifyScripts(spec.TX())
52+
tx := spec.TX()
53+
ok, err := spv.VerifyScripts(tx)
5354

5455
// then:
5556
require.NoError(t, err)

engine/transaction/outlines/internal/inputs/inputs_query_composer.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,12 @@ func (c *inputsQueryComposer) build(db *gorm.DB) *gorm.DB {
2222
utxoWithMinChange := c.searchForMinimalChangeValue(db, utxoWithChange)
2323
selectedOutpoints := c.chooseInputsToCoverOutputsAndFeesAndHaveMinimalChange(db, utxoWithMinChange)
2424

25-
res := db.Model(&database.UserUtxos{}).Where("(tx_id, vout) in (?)", selectedOutpoints)
25+
res := db.Model(&database.UserUTXO{}).Where("(tx_id, vout) in (?)", selectedOutpoints)
2626
return res
2727
}
2828

2929
func (c *inputsQueryComposer) utxos(db *gorm.DB) *gorm.DB {
30-
return db.Model(&database.UserUtxos{}).
30+
return db.Model(&database.UserUTXO{}).
3131
Select(
3232
txIdColumn,
3333
voutColumn,
@@ -60,11 +60,11 @@ func (c *inputsQueryComposer) searchForMinimalChangeValue(db *gorm.DB, utxoWithC
6060
}
6161

6262
func (c *inputsQueryComposer) feeCalculatedWithChangeOutput() string {
63-
return fmt.Sprintf("ceil((sum(unlocking_script_estimated_size) over (order by touched_at ASC, created_at ASC, tx_id ASC, vout ASC) + %d + %d) / cast(%d as float)) * %d as fee_with_change_output", c.txWithoutInputsSize, estimatedChangeOutputSize, c.feeUnit.Bytes, c.feeUnit.Satoshis)
63+
return fmt.Sprintf("ceil((sum(estimated_input_size) over (order by touched_at ASC, created_at ASC, tx_id ASC, vout ASC) + %d + %d) / cast(%d as float)) * %d as fee_with_change_output", c.txWithoutInputsSize, estimatedChangeOutputSize, c.feeUnit.Bytes, c.feeUnit.Satoshis)
6464
}
6565

6666
func (c *inputsQueryComposer) feeCalculatedWithoutChangeOutput() string {
67-
return fmt.Sprintf("ceil((sum(unlocking_script_estimated_size) over (order by touched_at ASC, created_at ASC, tx_id ASC, vout ASC) + %d) / cast(%d as float)) * %d as fee_no_change_output", c.txWithoutInputsSize, c.feeUnit.Bytes, c.feeUnit.Satoshis)
67+
return fmt.Sprintf("ceil((sum(estimated_input_size) over (order by touched_at ASC, created_at ASC, tx_id ASC, vout ASC) + %d) / cast(%d as float)) * %d as fee_no_change_output", c.txWithoutInputsSize, c.feeUnit.Bytes, c.feeUnit.Satoshis)
6868
}
6969

7070
func (c *inputsQueryComposer) remainingValue() string {

engine/transaction/outlines/internal/inputs/selector.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ const voutColumn = "vout"
1616

1717
// Selector is a service that selects inputs for transaction.
1818
type Selector interface {
19-
SelectInputsForTransaction(ctx context.Context, userID string, satoshis bsv.Satoshis, byteSizeOfTxBeforeAddingSelectedInputs uint64) ([]*database.UserUtxos, error)
19+
SelectInputsForTransaction(ctx context.Context, userID string, satoshis bsv.Satoshis, byteSizeOfTxBeforeAddingSelectedInputs uint64) ([]*database.UserUTXO, error)
2020
}
2121

2222
const (
@@ -39,7 +39,7 @@ func NewSelector(db *gorm.DB, feeUnit bsv.FeeUnit) Selector {
3939
}
4040
}
4141

42-
func (r *sqlInputsSelector) SelectInputsForTransaction(ctx context.Context, userID string, outputsTotalValue bsv.Satoshis, byteSizeOfTxWithoutInputs uint64) (utxos []*database.UserUtxos, err error) {
42+
func (r *sqlInputsSelector) SelectInputsForTransaction(ctx context.Context, userID string, outputsTotalValue bsv.Satoshis, byteSizeOfTxWithoutInputs uint64) (utxos []*database.UserUTXO, err error) {
4343
err = r.db.WithContext(ctx).Transaction(func(db *gorm.DB) error {
4444
inputsQuery := r.buildQueryForInputs(db, userID, outputsTotalValue, byteSizeOfTxWithoutInputs)
4545

@@ -78,10 +78,10 @@ func (r *sqlInputsSelector) buildQueryForInputs(db *gorm.DB, userID string, outp
7878
return composer.build(db)
7979
}
8080

81-
func (r *sqlInputsSelector) buildUpdateTouchedAtQuery(db *gorm.DB, utxos []*database.UserUtxos) *gorm.DB {
81+
func (r *sqlInputsSelector) buildUpdateTouchedAtQuery(db *gorm.DB, utxos []*database.UserUTXO) *gorm.DB {
8282
outpoints := make([][]any, 0, len(utxos))
8383
for _, utxo := range utxos {
8484
outpoints = append(outpoints, []any{utxo.TxID, utxo.Vout})
8585
}
86-
return db.Model(&database.UserUtxos{}).Where("(tx_id, vout) in (?)", outpoints)
86+
return db.Model(&database.UserUTXO{}).Where("(tx_id, vout) in (?)", outpoints)
8787
}

0 commit comments

Comments
 (0)