Skip to content

Commit c0de5c2

Browse files
committed
db/itest: cover ApplyTxBatch conformance
Add shared itests verifying ApplyTxBatch stores a tx and advances the sync tip, and confirms a tx in the same block it syncs to.
1 parent 9fc9704 commit c0de5c2

1 file changed

Lines changed: 223 additions & 0 deletions

File tree

wallet/internal/db/itest/tx_utxo_store_test.go

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3067,3 +3067,226 @@ func TestApplyTxBatchChildBeforeParent(t *testing.T) {
30673067
Hash: parentTx.TxHash(), Index: 0,
30683068
}))
30693069
}
3070+
3071+
// TestApplyTxBatchStoresTxAndSyncTip verifies that a runtime batch can persist
3072+
// transaction history and advance the wallet sync tip atomically.
3073+
func TestApplyTxBatchStoresTxAndSyncTip(t *testing.T) {
3074+
t.Parallel()
3075+
3076+
store := NewTestStore(t)
3077+
walletName := "wallet-apply-tx-batch"
3078+
walletID := newWallet(t, store, walletName)
3079+
createDerivedAccount(t, store, walletID, db.KeyScopeBIP0084, "default")
3080+
3081+
addr := newDerivedAddress(
3082+
t, store, walletID, db.KeyScopeBIP0084, "default", false,
3083+
)
3084+
tx := newRegularTx(
3085+
[]wire.OutPoint{randomOutPoint()},
3086+
[]*wire.TxOut{{Value: 7000, PkScript: addr.ScriptPubKey}},
3087+
)
3088+
syncedTo := NewBlockFixture(212)
3089+
3090+
err := store.ApplyTxBatch(t.Context(), db.TxBatchParams{
3091+
WalletID: walletID,
3092+
Transactions: []db.CreateTxParams{{
3093+
WalletID: walletID,
3094+
Tx: tx,
3095+
Received: time.Unix(1710000150, 0),
3096+
Status: db.TxStatusPending,
3097+
Credits: map[uint32]btcutil.Address{0: nil},
3098+
}},
3099+
SyncedTo: &syncedTo,
3100+
})
3101+
require.NoError(t, err)
3102+
3103+
txInfo, err := store.GetTx(t.Context(), db.GetTxQuery{
3104+
WalletID: walletID,
3105+
Txid: tx.TxHash(),
3106+
})
3107+
require.NoError(t, err)
3108+
require.Equal(t, db.TxStatusPending, txInfo.Status)
3109+
require.Nil(t, txInfo.Block)
3110+
require.True(t, walletUtxoExists(t, store, walletID, wire.OutPoint{
3111+
Hash: tx.TxHash(), Index: 0,
3112+
}))
3113+
3114+
walletInfo, err := store.GetWallet(t.Context(), walletName)
3115+
require.NoError(t, err)
3116+
require.NotNil(t, walletInfo.SyncedTo)
3117+
require.Equal(t, syncedTo.Hash, walletInfo.SyncedTo.Hash)
3118+
require.Equal(t, syncedTo.Height, walletInfo.SyncedTo.Height)
3119+
require.Equal(t, syncedTo.Timestamp.Unix(),
3120+
walletInfo.SyncedTo.Timestamp.Unix())
3121+
}
3122+
3123+
// TestApplyTxBatchConfirmsTxInSameBlock verifies that a batch can record a
3124+
// transaction confirmed in the very block the same batch introduces as the new
3125+
// sync tip. The confirming block row does not exist before the batch, so the
3126+
// batch must create the sync-tip block before recording the confirmed
3127+
// transaction; otherwise the confirmed insert fails with ErrBlockNotFound.
3128+
func TestApplyTxBatchConfirmsTxInSameBlock(t *testing.T) {
3129+
t.Parallel()
3130+
3131+
store := NewTestStore(t)
3132+
walletName := "wallet-apply-tx-batch-confirmed"
3133+
walletID := newWallet(t, store, walletName)
3134+
createDerivedAccount(t, store, walletID, db.KeyScopeBIP0084, "default")
3135+
3136+
addr := newDerivedAddress(
3137+
t, store, walletID, db.KeyScopeBIP0084, "default", false,
3138+
)
3139+
tx := newRegularTx(
3140+
[]wire.OutPoint{randomOutPoint()},
3141+
[]*wire.TxOut{{Value: 7000, PkScript: addr.ScriptPubKey}},
3142+
)
3143+
3144+
// The confirming block is also the batch's new sync tip. It is not
3145+
// inserted ahead of time, so the batch itself must create it before the
3146+
// confirmed transaction is recorded.
3147+
block := NewBlockFixture(213)
3148+
3149+
err := store.ApplyTxBatch(t.Context(), db.TxBatchParams{
3150+
WalletID: walletID,
3151+
Transactions: []db.CreateTxParams{{
3152+
WalletID: walletID,
3153+
Tx: tx,
3154+
Received: time.Unix(1710000160, 0),
3155+
Block: &block,
3156+
Status: db.TxStatusPublished,
3157+
Credits: map[uint32]btcutil.Address{0: nil},
3158+
}},
3159+
SyncedTo: &block,
3160+
})
3161+
require.NoError(t, err)
3162+
3163+
// The transaction is recorded as confirmed in the batch's block and its
3164+
// credited output is in the wallet UTXO set.
3165+
txInfo, err := store.GetTx(t.Context(), db.GetTxQuery{
3166+
WalletID: walletID,
3167+
Txid: tx.TxHash(),
3168+
})
3169+
require.NoError(t, err)
3170+
require.Equal(t, db.TxStatusPublished, txInfo.Status)
3171+
require.NotNil(t, txInfo.Block)
3172+
require.Equal(t, block.Height, txInfo.Block.Height)
3173+
require.Equal(t, block.Hash, txInfo.Block.Hash)
3174+
require.True(t, walletUtxoExists(t, store, walletID, wire.OutPoint{
3175+
Hash: tx.TxHash(), Index: 0,
3176+
}))
3177+
3178+
// The sync tip advanced to the same block.
3179+
walletInfo, err := store.GetWallet(t.Context(), walletName)
3180+
require.NoError(t, err)
3181+
require.NotNil(t, walletInfo.SyncedTo)
3182+
require.Equal(t, block.Height, walletInfo.SyncedTo.Height)
3183+
require.Equal(t, block.Hash, walletInfo.SyncedTo.Hash)
3184+
}
3185+
3186+
// TestApplyTxBatchRejectsMismatchedWalletID verifies that a batch is rejected
3187+
// when any transaction is owned by a wallet other than the batch wallet, and
3188+
// that the rejection commits nothing: the sync tip is not advanced and no
3189+
// transaction row is written.
3190+
func TestApplyTxBatchRejectsMismatchedWalletID(t *testing.T) {
3191+
t.Parallel()
3192+
3193+
store := NewTestStore(t)
3194+
walletName := "wallet-apply-tx-batch-mismatch"
3195+
walletID := newWallet(t, store, walletName)
3196+
createDerivedAccount(t, store, walletID, db.KeyScopeBIP0084, "default")
3197+
3198+
addr := newDerivedAddress(
3199+
t, store, walletID, db.KeyScopeBIP0084, "default", false,
3200+
)
3201+
tx := newRegularTx(
3202+
[]wire.OutPoint{randomOutPoint()},
3203+
[]*wire.TxOut{{Value: 7000, PkScript: addr.ScriptPubKey}},
3204+
)
3205+
syncedTo := NewBlockFixture(214)
3206+
3207+
// The batch targets walletID, but the lone transaction claims a different
3208+
// wallet. The whole batch must be rejected before any write.
3209+
err := store.ApplyTxBatch(t.Context(), db.TxBatchParams{
3210+
WalletID: walletID,
3211+
Transactions: []db.CreateTxParams{{
3212+
WalletID: walletID + 99,
3213+
Tx: tx,
3214+
Received: time.Unix(1710000170, 0),
3215+
Status: db.TxStatusPending,
3216+
Credits: map[uint32]btcutil.Address{0: nil},
3217+
}},
3218+
SyncedTo: &syncedTo,
3219+
})
3220+
require.ErrorIs(t, err, db.ErrInvalidParam)
3221+
3222+
// The sync tip was not advanced: the wallet is still unsynced.
3223+
walletInfo, err := store.GetWallet(t.Context(), walletName)
3224+
require.NoError(t, err)
3225+
require.Nil(t, walletInfo.SyncedTo)
3226+
3227+
// No transaction row was written for either wallet.
3228+
_, err = store.GetTx(t.Context(), db.GetTxQuery{
3229+
WalletID: walletID,
3230+
Txid: tx.TxHash(),
3231+
})
3232+
require.ErrorIs(t, err, db.ErrTxNotFound)
3233+
}
3234+
3235+
// TestApplyTxBatchRejectsNilTx verifies that a multi-transaction batch with one
3236+
// nil Tx is rejected with ErrInvalidParam rather than panicking. ApplyTxBatch
3237+
// reorders the batch parents-first before applying it, and that sort
3238+
// dereferences each transaction's Tx, so a nil member must be caught up front;
3239+
// the rejection must also commit nothing.
3240+
func TestApplyTxBatchRejectsNilTx(t *testing.T) {
3241+
t.Parallel()
3242+
3243+
store := NewTestStore(t)
3244+
walletName := "wallet-apply-tx-batch-nil-tx"
3245+
walletID := newWallet(t, store, walletName)
3246+
createDerivedAccount(t, store, walletID, db.KeyScopeBIP0084, "default")
3247+
3248+
addr := newDerivedAddress(
3249+
t, store, walletID, db.KeyScopeBIP0084, "default", false,
3250+
)
3251+
tx := newRegularTx(
3252+
[]wire.OutPoint{randomOutPoint()},
3253+
[]*wire.TxOut{{Value: 7000, PkScript: addr.ScriptPubKey}},
3254+
)
3255+
syncedTo := NewBlockFixture(215)
3256+
3257+
// The batch carries a valid transaction and a second one with a nil Tx.
3258+
// The parents-first sort runs before per-tx request validation, so the nil
3259+
// member must be rejected by the up-front guard rather than panicking.
3260+
err := store.ApplyTxBatch(t.Context(), db.TxBatchParams{
3261+
WalletID: walletID,
3262+
Transactions: []db.CreateTxParams{
3263+
{
3264+
WalletID: walletID,
3265+
Tx: tx,
3266+
Received: time.Unix(1710000210, 0),
3267+
Status: db.TxStatusPending,
3268+
Credits: map[uint32]btcutil.Address{0: nil},
3269+
},
3270+
{
3271+
WalletID: walletID,
3272+
Tx: nil,
3273+
Received: time.Unix(1710000211, 0),
3274+
Status: db.TxStatusPending,
3275+
},
3276+
},
3277+
SyncedTo: &syncedTo,
3278+
})
3279+
require.ErrorIs(t, err, db.ErrInvalidParam)
3280+
3281+
// The sync tip was not advanced and the valid transaction was not written:
3282+
// the whole batch is rejected before any write.
3283+
walletInfo, err := store.GetWallet(t.Context(), walletName)
3284+
require.NoError(t, err)
3285+
require.Nil(t, walletInfo.SyncedTo)
3286+
3287+
_, err = store.GetTx(t.Context(), db.GetTxQuery{
3288+
WalletID: walletID,
3289+
Txid: tx.TxHash(),
3290+
})
3291+
require.ErrorIs(t, err, db.ErrTxNotFound)
3292+
}

0 commit comments

Comments
 (0)