From 6162e05c703a5a16f3ede793bac92eaeee75bec9 Mon Sep 17 00:00:00 2001 From: ffranr Date: Thu, 18 Jul 2024 23:06:35 +0100 Subject: [PATCH 1/7] tapdb: add proof_delivery_complete and position to transfer outputs Add the `proof_delivery_complete` and `position` columns to the `asset_transfer_outputs` table. The `position` column indicates the position of the output in the transfer output list. The position and anchor outpoint uniquely identify a transfer output. This change updates the row insertion and retrieval SQL statements for this table. Additionally, new SQL statements are included for modifying these new columns. --- tapdb/migrations.go | 2 +- ..._transfer_outputs_proof_delivered.down.sql | 10 +++++ ...22_transfer_outputs_proof_delivered.up.sql | 28 +++++++++++++ tapdb/sqlc/models.go | 2 + tapdb/sqlc/querier.go | 1 + tapdb/sqlc/queries/transfers.sql | 21 ++++++++-- tapdb/sqlc/transfers.sql.go | 41 +++++++++++++++++-- 7 files changed, 96 insertions(+), 9 deletions(-) create mode 100644 tapdb/sqlc/migrations/000022_transfer_outputs_proof_delivered.down.sql create mode 100644 tapdb/sqlc/migrations/000022_transfer_outputs_proof_delivered.up.sql diff --git a/tapdb/migrations.go b/tapdb/migrations.go index 51aa7cfda..4966fb73c 100644 --- a/tapdb/migrations.go +++ b/tapdb/migrations.go @@ -22,7 +22,7 @@ const ( // daemon. // // NOTE: This MUST be updated when a new migration is added. - LatestMigrationVersion = 21 + LatestMigrationVersion = 22 ) // MigrationTarget is a functional option that can be passed to applyMigrations diff --git a/tapdb/sqlc/migrations/000022_transfer_outputs_proof_delivered.down.sql b/tapdb/sqlc/migrations/000022_transfer_outputs_proof_delivered.down.sql new file mode 100644 index 000000000..3c914cb15 --- /dev/null +++ b/tapdb/sqlc/migrations/000022_transfer_outputs_proof_delivered.down.sql @@ -0,0 +1,10 @@ +-- Remove the unique constraint on the `transfer_id` and `position` columns in +-- the `asset_transfer_outputs` table. +DROP INDEX asset_transfer_outputs_transfer_id_position_unique; + +-- Remove the `proof_delivery_complete` column from the `asset_transfer_outputs` +-- table. +ALTER TABLE asset_transfer_outputs DROP COLUMN proof_delivery_complete; + +-- Remove the `position` column from the `asset_transfer_outputs` table. +ALTER TABLE asset_transfer_outputs DROP COLUMN position; \ No newline at end of file diff --git a/tapdb/sqlc/migrations/000022_transfer_outputs_proof_delivered.up.sql b/tapdb/sqlc/migrations/000022_transfer_outputs_proof_delivered.up.sql new file mode 100644 index 000000000..e7543022a --- /dev/null +++ b/tapdb/sqlc/migrations/000022_transfer_outputs_proof_delivered.up.sql @@ -0,0 +1,28 @@ +-- Add a column to track if the proof has been delivered for an asset transfer +-- output. +ALTER TABLE asset_transfer_outputs +ADD COLUMN proof_delivery_complete BOOL; + +-- Add column `position` which indicates the index of the output in the list of +-- outputs for a given transfer. This index position in conjunction with the +-- transfer id can be used to uniquely identify a transfer output. +-- +-- We'll be inserting an actual value in the next query, so we just start +-- with -1. +ALTER TABLE asset_transfer_outputs +ADD COLUMN position INTEGER NOT NULL DEFAULT -1; + +-- Update the position to be the same as the output id for existing entries. +-- We'll use the position integer as a uniquely identifiable number of an output +-- within a transfer, so setting the default to the output_id is just to make +-- sure we have a unique value that also satisfies the unique constraint we add +-- below. +UPDATE asset_transfer_outputs SET position = CAST(output_id AS INTEGER) +WHERE position = -1; + +-- We enforce a unique constraint such that for a given transfer, the position +-- of an output is unique. +CREATE UNIQUE INDEX asset_transfer_outputs_transfer_id_position_unique +ON asset_transfer_outputs ( + transfer_id, position +); diff --git a/tapdb/sqlc/models.go b/tapdb/sqlc/models.go index ed23b3b83..4a7aba095 100644 --- a/tapdb/sqlc/models.go +++ b/tapdb/sqlc/models.go @@ -135,6 +135,8 @@ type AssetTransferOutput struct { ProofCourierAddr []byte LockTime sql.NullInt32 RelativeLockTime sql.NullInt32 + ProofDeliveryComplete sql.NullBool + Position int32 } type AssetWitness struct { diff --git a/tapdb/sqlc/querier.go b/tapdb/sqlc/querier.go index 17ace6b90..2cb90e03f 100644 --- a/tapdb/sqlc/querier.go +++ b/tapdb/sqlc/querier.go @@ -148,6 +148,7 @@ type Querier interface { ReAnchorPassiveAssets(ctx context.Context, arg ReAnchorPassiveAssetsParams) error SetAddrManaged(ctx context.Context, arg SetAddrManagedParams) error SetAssetSpent(ctx context.Context, arg SetAssetSpentParams) (int64, error) + SetTransferOutputProofDeliveryStatus(ctx context.Context, arg SetTransferOutputProofDeliveryStatusParams) error UniverseLeaves(ctx context.Context) ([]UniverseLeafe, error) UniverseRoots(ctx context.Context, arg UniverseRootsParams) ([]UniverseRootsRow, error) UpdateBatchGenesisTx(ctx context.Context, arg UpdateBatchGenesisTxParams) error diff --git a/tapdb/sqlc/queries/transfers.sql b/tapdb/sqlc/queries/transfers.sql index 5322f77c0..1c9a3d211 100644 --- a/tapdb/sqlc/queries/transfers.sql +++ b/tapdb/sqlc/queries/transfers.sql @@ -23,11 +23,24 @@ INSERT INTO asset_transfer_outputs ( amount, serialized_witnesses, split_commitment_root_hash, split_commitment_root_value, proof_suffix, num_passive_assets, output_type, proof_courier_addr, asset_version, lock_time, - relative_lock_time + relative_lock_time, proof_delivery_complete, position ) VALUES ( - $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15 + $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17 ); +-- name: SetTransferOutputProofDeliveryStatus :exec +WITH target(output_id) AS ( + SELECT output_id + FROM asset_transfer_outputs output + JOIN managed_utxos + ON output.anchor_utxo = managed_utxos.utxo_id + WHERE managed_utxos.outpoint = @serialized_anchor_outpoint + AND output.position = @position +) +UPDATE asset_transfer_outputs +SET proof_delivery_complete = @delivery_complete +WHERE output_id = (SELECT output_id FROM target); + -- name: QueryAssetTransfers :many SELECT id, height_hint, txns.txid, transfer_time_unix @@ -55,8 +68,8 @@ ORDER BY input_id; SELECT output_id, proof_suffix, amount, serialized_witnesses, script_key_local, split_commitment_root_hash, split_commitment_root_value, num_passive_assets, - output_type, proof_courier_addr, asset_version, lock_time, - relative_lock_time, + output_type, proof_courier_addr, proof_delivery_complete, position, + asset_version, lock_time, relative_lock_time, utxos.utxo_id AS anchor_utxo_id, utxos.outpoint AS anchor_outpoint, utxos.amt_sats AS anchor_value, diff --git a/tapdb/sqlc/transfers.sql.go b/tapdb/sqlc/transfers.sql.go index a776700e3..0e8a17040 100644 --- a/tapdb/sqlc/transfers.sql.go +++ b/tapdb/sqlc/transfers.sql.go @@ -125,8 +125,8 @@ const fetchTransferOutputs = `-- name: FetchTransferOutputs :many SELECT output_id, proof_suffix, amount, serialized_witnesses, script_key_local, split_commitment_root_hash, split_commitment_root_value, num_passive_assets, - output_type, proof_courier_addr, asset_version, lock_time, - relative_lock_time, + output_type, proof_courier_addr, proof_delivery_complete, position, + asset_version, lock_time, relative_lock_time, utxos.utxo_id AS anchor_utxo_id, utxos.outpoint AS anchor_outpoint, utxos.amt_sats AS anchor_value, @@ -168,6 +168,8 @@ type FetchTransferOutputsRow struct { NumPassiveAssets int32 OutputType int16 ProofCourierAddr []byte + ProofDeliveryComplete sql.NullBool + Position int32 AssetVersion int32 LockTime sql.NullInt32 RelativeLockTime sql.NullInt32 @@ -210,6 +212,8 @@ func (q *Queries) FetchTransferOutputs(ctx context.Context, transferID int64) ([ &i.NumPassiveAssets, &i.OutputType, &i.ProofCourierAddr, + &i.ProofDeliveryComplete, + &i.Position, &i.AssetVersion, &i.LockTime, &i.RelativeLockTime, @@ -303,9 +307,9 @@ INSERT INTO asset_transfer_outputs ( amount, serialized_witnesses, split_commitment_root_hash, split_commitment_root_value, proof_suffix, num_passive_assets, output_type, proof_courier_addr, asset_version, lock_time, - relative_lock_time + relative_lock_time, proof_delivery_complete, position ) VALUES ( - $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15 + $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17 ) ` @@ -325,6 +329,8 @@ type InsertAssetTransferOutputParams struct { AssetVersion int32 LockTime sql.NullInt32 RelativeLockTime sql.NullInt32 + ProofDeliveryComplete sql.NullBool + Position int32 } func (q *Queries) InsertAssetTransferOutput(ctx context.Context, arg InsertAssetTransferOutputParams) error { @@ -344,6 +350,8 @@ func (q *Queries) InsertAssetTransferOutput(ctx context.Context, arg InsertAsset arg.AssetVersion, arg.LockTime, arg.RelativeLockTime, + arg.ProofDeliveryComplete, + arg.Position, ) return err } @@ -584,3 +592,28 @@ func (q *Queries) ReAnchorPassiveAssets(ctx context.Context, arg ReAnchorPassive _, err := q.db.ExecContext(ctx, reAnchorPassiveAssets, arg.NewAnchorUtxoID, arg.AssetID) return err } + +const setTransferOutputProofDeliveryStatus = `-- name: SetTransferOutputProofDeliveryStatus :exec +WITH target(output_id) AS ( + SELECT output_id + FROM asset_transfer_outputs output + JOIN managed_utxos + ON output.anchor_utxo = managed_utxos.utxo_id + WHERE managed_utxos.outpoint = $2 + AND output.position = $3 +) +UPDATE asset_transfer_outputs +SET proof_delivery_complete = $1 +WHERE output_id = (SELECT output_id FROM target) +` + +type SetTransferOutputProofDeliveryStatusParams struct { + DeliveryComplete sql.NullBool + SerializedAnchorOutpoint []byte + Position int32 +} + +func (q *Queries) SetTransferOutputProofDeliveryStatus(ctx context.Context, arg SetTransferOutputProofDeliveryStatusParams) error { + _, err := q.db.ExecContext(ctx, setTransferOutputProofDeliveryStatus, arg.DeliveryComplete, arg.SerializedAnchorOutpoint, arg.Position) + return err +} From 2c3b072d662c6c46e6ac7854a9d8255c43af6383 Mon Sep 17 00:00:00 2001 From: ffranr Date: Fri, 26 Jul 2024 22:13:36 +0100 Subject: [PATCH 2/7] tapfreighter: add method ShouldDeliverProof to struct TransferOutput ShouldDeliverProof returns true if a proof corresponding to the subject transfer output should be delivered to a peer. --- tapfreighter/interface.go | 42 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/tapfreighter/interface.go b/tapfreighter/interface.go index cadca20bb..32bcb20c4 100644 --- a/tapfreighter/interface.go +++ b/tapfreighter/interface.go @@ -248,6 +248,48 @@ type TransferOutput struct { ProofCourierAddr []byte } +// ShouldDeliverProof returns true if a proof corresponding to the subject +// transfer output should be delivered to a peer. +func (out *TransferOutput) ShouldDeliverProof() (bool, error) { + // If the proof courier address is unspecified, we don't need to deliver + // a proof. + if len(out.ProofCourierAddr) == 0 { + return false, nil + } + + // The proof courier address may have been specified in error, in which + // case we will conduct further checks to determine if a proof should be + // delivered. + // + // If the script key is un-spendable, we don't need to deliver a proof. + unSpendable, err := out.ScriptKey.IsUnSpendable() + if err != nil { + return false, fmt.Errorf("error checking if script key is "+ + "unspendable: %w", err) + } + + if unSpendable { + return false, nil + } + + // If this is an output that is going to our own node/wallet, we don't + // need to deliver a proof. + if out.ScriptKey.TweakedScriptKey != nil && out.ScriptKeyLocal { + return false, nil + } + + // If the script key is a burn key, we don't need to deliver a proof. + if len(out.WitnessData) > 0 && asset.IsBurnKey( + out.ScriptKey.PubKey, out.WitnessData[0], + ) { + + return false, nil + } + + // At this point, we should deliver a proof. + return true, nil +} + // OutboundParcel represents the database level delta of an outbound Taproot // Asset parcel (outbound spend). A spend will destroy a series of assets listed // as inputs, and re-create them as new outputs. Along the way some assets may From 995f6d84fc9aa2c3c505230afd88c28fc1516434 Mon Sep 17 00:00:00 2001 From: ffranr Date: Fri, 19 Jul 2024 10:07:02 +0100 Subject: [PATCH 3/7] tapdb+tapfreighter: set and retrieve fields from new columns Set and retrieve the `proof_delivery_complete` and `position` columns from the `asset_transfer_outputs` table. --- tapdb/assets_store.go | 84 ++++++++++++++++++++++++++++++--------- tapfreighter/interface.go | 12 ++++++ tapfreighter/parcel.go | 16 ++++++-- 3 files changed, 90 insertions(+), 22 deletions(-) diff --git a/tapdb/assets_store.go b/tapdb/assets_store.go index 78c29bf86..0c578e798 100644 --- a/tapdb/assets_store.go +++ b/tapdb/assets_store.go @@ -2401,20 +2401,50 @@ func insertAssetTransferOutput(ctx context.Context, q ActiveAssetsStore, return fmt.Errorf("unable to insert script key: %w", err) } + // Now we will mark the output proof as undelivered if it is intended + // for a counterpart. + // + // If the transfer output proof is not intended for a counterpart the + // `proofDeliveryComplete` field will be left as NULL. Otherwise, we set + // it to false to indicate that the proof has not been delivered yet. + shouldDeliverProof, err := output.ShouldDeliverProof() + if err != nil { + return fmt.Errorf("unable to determine if proof should be "+ + "delivery for given transfer output: %w", err) + } + + var proofDeliveryComplete sql.NullBool + if shouldDeliverProof { + proofDeliveryComplete = sql.NullBool{ + Bool: false, + Valid: true, + } + } + + // Check if position value can be stored in a 32-bit integer. Type cast + // if possible, otherwise return an error. + if output.Position > math.MaxInt32 { + return fmt.Errorf("position value %d is too large for db "+ + "storage", output.Position) + } + position := int32(output.Position) + dbOutput := NewTransferOutput{ - TransferID: transferID, - AnchorUtxo: newUtxoID, - ScriptKey: scriptKeyID, - ScriptKeyLocal: output.ScriptKeyLocal, - Amount: int64(output.Amount), - LockTime: sqlInt32(output.LockTime), - RelativeLockTime: sqlInt32(output.RelativeLockTime), - AssetVersion: int32(output.AssetVersion), - SerializedWitnesses: witnessBuf.Bytes(), - ProofSuffix: output.ProofSuffix, - NumPassiveAssets: int32(output.Anchor.NumPassiveAssets), - OutputType: int16(output.Type), - ProofCourierAddr: output.ProofCourierAddr, + TransferID: transferID, + AnchorUtxo: newUtxoID, + ScriptKey: scriptKeyID, + ScriptKeyLocal: output.ScriptKeyLocal, + Amount: int64(output.Amount), + LockTime: sqlInt32(output.LockTime), + RelativeLockTime: sqlInt32(output.RelativeLockTime), + AssetVersion: int32(output.AssetVersion), + SerializedWitnesses: witnessBuf.Bytes(), + ProofSuffix: output.ProofSuffix, + NumPassiveAssets: int32(output.Anchor.NumPassiveAssets), + OutputType: int16(output.Type), + ProofCourierAddr: output.ProofCourierAddr, + ProofDeliveryComplete: proofDeliveryComplete, + Position: position, } // There might not have been a split, so we can't rely on the split root @@ -2526,6 +2556,22 @@ func fetchAssetTransferOutputs(ctx context.Context, q ActiveAssetsStore, outputAnchor.CommitmentVersion = fn.Ptr(dbRootVersion) } + // Parse the proof deliver complete flag from the database. + var proofDeliveryComplete fn.Option[bool] + if dbOut.ProofDeliveryComplete.Valid { + proofDeliveryComplete = fn.Some( + dbOut.ProofDeliveryComplete.Bool, + ) + } + + vOutputType := tappsbt.VOutputType(dbOut.OutputType) + + // Ensure the position value is valid. + if dbOut.Position < 0 { + return nil, fmt.Errorf("invalid position value in "+ + "db: %d", dbOut.Position) + } + outputs[idx] = tapfreighter.TransferOutput{ Anchor: outputAnchor, Amount: uint64(dbOut.Amount), @@ -2549,9 +2595,11 @@ func fetchAssetTransferOutputs(ctx context.Context, q ActiveAssetsStore, splitRootHash, uint64(dbOut.SplitCommitmentRootValue.Int64), ), - ProofSuffix: dbOut.ProofSuffix, - Type: tappsbt.VOutputType(dbOut.OutputType), - ProofCourierAddr: dbOut.ProofCourierAddr, + ProofSuffix: dbOut.ProofSuffix, + Type: vOutputType, + ProofCourierAddr: dbOut.ProofCourierAddr, + ProofDeliveryComplete: proofDeliveryComplete, + Position: uint64(dbOut.Position), } err = readOutPoint( @@ -2794,10 +2842,10 @@ func (a *AssetStore) ConfirmParcelDelivery(ctx context.Context, !out.ScriptKeyLocal && !isKnown log.Tracef("Skip asset creation for "+ - "output %d?: %v, scriptKey=%x, "+ + "output %d?: %v, position=%v, scriptKey=%x, "+ "isTombstone=%v, isBurn=%v, "+ "scriptKeyLocal=%v, scriptKeyKnown=%v", - idx, skipAssetCreation, + idx, skipAssetCreation, out.Position, scriptPubKey.SerializeCompressed(), isTombstone, isBurn, out.ScriptKeyLocal, isKnown) diff --git a/tapfreighter/interface.go b/tapfreighter/interface.go index 32bcb20c4..bd7ff46ff 100644 --- a/tapfreighter/interface.go +++ b/tapfreighter/interface.go @@ -246,6 +246,18 @@ type TransferOutput struct { // ProofCourierAddr is the bytes encoded proof courier service address // associated with this output. ProofCourierAddr []byte + + // ProofDeliveryComplete is a flag that indicates whether the proof + // delivery for this output is complete. + // + // This field can take one of the following values: + // - None: A proof will not be delivered to a counterparty. + // - False: The proof has not yet been delivered successfully. + // - True: The proof has been delivered to the recipient. + ProofDeliveryComplete fn.Option[bool] + + // Position is the position of the output in the transfer output list. + Position uint64 } // ShouldDeliverProof returns true if a proof corresponding to the subject diff --git a/tapfreighter/parcel.go b/tapfreighter/parcel.go index f85dfdeb4..1bb079192 100644 --- a/tapfreighter/parcel.go +++ b/tapfreighter/parcel.go @@ -531,17 +531,24 @@ func ConvertToTransfer(currentHeight uint32, activeTransfers []*tappsbt.VPacket, } } + // The outputPosition represents the index of the output within the list + // of output transfers. It is continuously incremented across all + // outputs and virtual packets. + outputPosition := uint64(0) + for pIdx := range activeTransfers { vPkt := activeTransfers[pIdx] - for idx := range vPkt.Outputs { + for vPktOutputIdx := range vPkt.Outputs { tOut, err := transferOutput( - vPkt, idx, anchorTx, passiveAssets, isLocalKey, + vPkt, vPktOutputIdx, outputPosition, anchorTx, + passiveAssets, isLocalKey, ) if err != nil { return nil, fmt.Errorf("unable to convert "+ - "output %d: %w", idx, err) + "output %d: %w", vPktOutputIdx, err) } + outputPosition += 1 parcel.Outputs = append(parcel.Outputs, *tOut) } @@ -566,7 +573,7 @@ func transferInput(vIn *tappsbt.VInput) (*TransferInput, error) { // transferOutput creates a TransferOutput from a virtual output and the anchor // packet. -func transferOutput(vPkt *tappsbt.VPacket, vOutIdx int, +func transferOutput(vPkt *tappsbt.VPacket, vOutIdx int, position uint64, anchorTx *tapsend.AnchorTransaction, passiveAssets []*tappsbt.VPacket, isLocalKey func(asset.ScriptKey) bool) (*TransferOutput, error) { @@ -612,6 +619,7 @@ func transferOutput(vPkt *tappsbt.VPacket, vOutIdx int, ProofSuffix: proofSuffixBuf.Bytes(), ProofCourierAddr: proofCourierAddrBytes, ScriptKeyLocal: isLocalKey(vOut.ScriptKey), + Position: position, }, nil } From 2841027661cf5f8158da4573465f6e76e90feddf Mon Sep 17 00:00:00 2001 From: ffranr Date: Fri, 26 Jul 2024 22:36:07 +0100 Subject: [PATCH 4/7] tapfreighter: simplify ChainPorter method transferReceiverProof Refactors the `transferReceiverProof` method in `ChainPorter` by utilizing the new `ShouldDeliverProof` method for transfer output. This change simplifies the method's implementation. Note: This refactoring is not related to the primary objectives of this series of commits. --- tapfreighter/chain_porter.go | 45 ++++++------------------------------ 1 file changed, 7 insertions(+), 38 deletions(-) diff --git a/tapfreighter/chain_porter.go b/tapfreighter/chain_porter.go index a7527d212..3085a4b6e 100644 --- a/tapfreighter/chain_porter.go +++ b/tapfreighter/chain_porter.go @@ -643,47 +643,16 @@ func (p *ChainPorter) transferReceiverProof(pkg *sendPackage) error { deliver := func(ctx context.Context, out TransferOutput) error { key := out.ScriptKey.PubKey - // If this is an output that is going to our own node/wallet, - // we don't need to transfer the proof. - if out.ScriptKey.TweakedScriptKey != nil && out.ScriptKeyLocal { - log.Debugf("Not transferring proof for local output "+ - "script key %x", key.SerializeCompressed()) - return nil - } - - // Un-spendable means this is a tombstone output resulting from - // a split. - unSpendable, err := out.ScriptKey.IsUnSpendable() + // We'll first check to see if the proof should be delivered. + shouldDeliverProof, err := out.ShouldDeliverProof() if err != nil { - return fmt.Errorf("error checking if script key is "+ - "unspendable: %w", err) - } - if unSpendable { - log.Debugf("Not transferring proof for un-spendable "+ - "output script key %x", - key.SerializeCompressed()) - return nil - } - - // Burns are also always kept local and not sent to any - // receiver. - if len(out.WitnessData) > 0 && asset.IsBurnKey( - out.ScriptKey.PubKey, out.WitnessData[0], - ) { - - log.Debugf("Not transferring proof for burn script "+ - "key %x", key.SerializeCompressed()) - return nil + return fmt.Errorf("error determining if proof should "+ + "be delivered: %w", err) } - // We can only deliver proofs for outputs that have a proof - // courier address. If an output doesn't have one, we assume it - // is an interactive send where the recipient is already aware - // of the proof or learns of it through another channel. - if len(out.ProofCourierAddr) == 0 { - log.Debugf("Not transferring proof for output with "+ - "script key %x as it has no proof courier "+ - "address", key.SerializeCompressed()) + if !shouldDeliverProof { + log.Debugf("Not delivering proof for output with "+ + "script key %x", key.SerializeCompressed()) return nil } From 6908372d70b1d9e2511a9e647160543fe7d702f1 Mon Sep 17 00:00:00 2001 From: ffranr Date: Fri, 19 Jul 2024 10:29:10 +0100 Subject: [PATCH 5/7] tapdb+tapfreighter: log proof delivery as confirmed Add a `ConfirmProofDelivery` method to the export log. This function is called after a proof has been successfully delivered to a remote peer. --- tapdb/assets_store.go | 48 ++++++++++++++++++++++++++++++++++++ tapfreighter/chain_porter.go | 14 ++++++++++- tapfreighter/interface.go | 4 +++ 3 files changed, 65 insertions(+), 1 deletion(-) diff --git a/tapdb/assets_store.go b/tapdb/assets_store.go index 0c578e798..bc2857414 100644 --- a/tapdb/assets_store.go +++ b/tapdb/assets_store.go @@ -126,6 +126,12 @@ type ( // output. NewTransferOutput = sqlc.InsertAssetTransferOutputParams + // OutputProofDeliveryStatus wraps the params needed to set the delivery + // status of a given output proof. + // + // nolint: lll + OutputProofDeliveryStatus = sqlc.SetTransferOutputProofDeliveryStatusParams + // NewPassiveAsset wraps the params needed to insert a new passive // asset. NewPassiveAsset = sqlc.InsertPassiveAssetParams @@ -278,6 +284,11 @@ type ActiveAssetsStore interface { InsertAssetTransferOutput(ctx context.Context, arg NewTransferOutput) error + // SetTransferOutputProofDeliveryStatus sets the delivery status of a + // given transfer output proof. + SetTransferOutputProofDeliveryStatus(ctx context.Context, + arg OutputProofDeliveryStatus) error + // FetchTransferInputs fetches the inputs to a given asset transfer. FetchTransferInputs(ctx context.Context, transferID int64) ([]TransferInputRow, error) @@ -2750,6 +2761,43 @@ func (a *AssetStore) QueryProofTransferLog(ctx context.Context, return timestamps, err } +// ConfirmProofDelivery marks a transfer output proof as successfully +// delivered to counterparty. +func (a *AssetStore) ConfirmProofDelivery(ctx context.Context, + anchorOutpoint wire.OutPoint, outputPosition uint64) error { + + // Serialize the anchor outpoint to bytes. + anchorOutpointBytes, err := encodeOutpoint(anchorOutpoint) + if err != nil { + return fmt.Errorf("unable to encode anchor outpoint: %w", err) + } + + // Ensure that the position value can be stored in a 32-bit integer. + // Type cast if possible, otherwise return an error. + if outputPosition > math.MaxInt32 { + return fmt.Errorf("position value is too large for db: %d", + outputPosition) + } + outPosition := int32(outputPosition) + + var writeTxOpts AssetStoreTxOptions + + err = a.db.ExecTx(ctx, &writeTxOpts, func(q ActiveAssetsStore) error { + params := OutputProofDeliveryStatus{ + DeliveryComplete: sqlBool(true), + SerializedAnchorOutpoint: anchorOutpointBytes, + Position: outPosition, + } + return q.SetTransferOutputProofDeliveryStatus(ctx, params) + }) + if err != nil { + return fmt.Errorf("failed to confirm transfer output proof "+ + "delivery status in db: %w", err) + } + + return nil +} + // ConfirmParcelDelivery marks a spend event on disk as confirmed. This updates // the on-chain reference information on disk to point to this new spend. func (a *AssetStore) ConfirmParcelDelivery(ctx context.Context, diff --git a/tapfreighter/chain_porter.go b/tapfreighter/chain_porter.go index 3085a4b6e..d201d1533 100644 --- a/tapfreighter/chain_porter.go +++ b/tapfreighter/chain_porter.go @@ -640,6 +640,8 @@ func (p *ChainPorter) transferReceiverProof(pkg *sendPackage) error { ctx, cancel := p.WithCtxQuitNoTimeout() defer cancel() + anchorTXID := pkg.OutboundPkg.AnchorTx.TxHash() + deliver := func(ctx context.Context, out TransferOutput) error { key := out.ScriptKey.PubKey @@ -720,6 +722,16 @@ func (p *ChainPorter) transferReceiverProof(pkg *sendPackage) error { "courier service: %w", err) } + // The proof has been successfully delivered to the receiver. + // Now, we will update our transfer log to reflect this. + err = p.cfg.ExportLog.ConfirmProofDelivery( + ctx, out.Anchor.OutPoint, out.Position, + ) + if err != nil { + return fmt.Errorf("unable to log proof delivery "+ + "confirmation: %w", err) + } + return nil } @@ -764,7 +776,7 @@ func (p *ChainPorter) transferReceiverProof(pkg *sendPackage) error { // At this point we have the confirmation signal, so we can mark the // parcel delivery as completed in the database. err = p.cfg.ExportLog.ConfirmParcelDelivery(ctx, &AssetConfirmEvent{ - AnchorTXID: pkg.OutboundPkg.AnchorTx.TxHash(), + AnchorTXID: anchorTXID, BlockHash: *pkg.TransferTxConfEvent.BlockHash, BlockHeight: int32(pkg.TransferTxConfEvent.BlockHeight), TxIndex: int32(pkg.TransferTxConfEvent.TxIndex), diff --git a/tapfreighter/interface.go b/tapfreighter/interface.go index bd7ff46ff..0f918e3f0 100644 --- a/tapfreighter/interface.go +++ b/tapfreighter/interface.go @@ -415,6 +415,10 @@ type ExportLog interface { // transactions for re-broadcast. PendingParcels(context.Context) ([]*OutboundParcel, error) + // ConfirmProofDelivery marks a transfer output proof as successfully + // transferred. + ConfirmProofDelivery(context.Context, wire.OutPoint, uint64) error + // ConfirmParcelDelivery marks a spend event on disk as confirmed. This // updates the on-chain reference information on disk to point to this // new spend. From d8c49b0043a4f4d0af3cfa54bfd80db02ecf7170 Mon Sep 17 00:00:00 2001 From: ffranr Date: Fri, 26 Jul 2024 23:36:48 +0100 Subject: [PATCH 6/7] tapdb: unit tests: set missing position fields in transfer outputs --- tapdb/assets_store_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tapdb/assets_store_test.go b/tapdb/assets_store_test.go index 680a952c5..960fe738c 100644 --- a/tapdb/assets_store_test.go +++ b/tapdb/assets_store_test.go @@ -1386,6 +1386,7 @@ func TestAssetExportLog(t *testing.T) { // The receiver wants a V0 asset version. AssetVersion: asset.V0, ProofSuffix: receiverBlob, + Position: 0, }, { Anchor: tapfreighter.Anchor{ Value: 1000, @@ -1420,6 +1421,7 @@ func TestAssetExportLog(t *testing.T) { // asset version. AssetVersion: asset.V1, ProofSuffix: senderBlob, + Position: 1, }}, } require.NoError(t, assetsStore.LogPendingParcel( From 25f0abadd23fecb55de1cd20c566b4897d34edb8 Mon Sep 17 00:00:00 2001 From: ffranr Date: Fri, 26 Jul 2024 23:41:06 +0100 Subject: [PATCH 7/7] tapdb: add unit test for proof delivery confirmation This commit introduces a unit test for the `SetTransferOutputProofDeliveryStatus` functionality to ensure proof delivery confirmation. --- tapdb/assets_store_test.go | 239 +++++++++++++++++++++++++++++++++++++ 1 file changed, 239 insertions(+) diff --git a/tapdb/assets_store_test.go b/tapdb/assets_store_test.go index 960fe738c..9bc657913 100644 --- a/tapdb/assets_store_test.go +++ b/tapdb/assets_store_test.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "crypto/sha256" + "database/sql" "math/rand" "sort" "testing" @@ -1897,3 +1898,241 @@ func TestFetchGroupedAssets(t *testing.T) { equalityCheck(allAssets[2].Asset, groupedAssets[1]) equalityCheck(allAssets[3].Asset, groupedAssets[2]) } + +// TestTransferOutputProofDeliveryStatus tests that we can properly set the +// proof delivery status of a transfer output. +func TestTransferOutputProofDeliveryStatus(t *testing.T) { + t.Parallel() + + // First, we'll create a new assets store. We'll use this to store the + // asset and the outbound parcel in the database. + _, assetsStore, db := newAssetStore(t) + ctx := context.Background() + + // Generate a single asset. + targetScriptKey := asset.NewScriptKeyBip86(keychain.KeyDescriptor{ + PubKey: test.RandPubKey(t), + KeyLocator: keychain.KeyLocator{ + Family: test.RandInt[keychain.KeyFamily](), + Index: uint32(test.RandInt[int32]()), + }, + }) + + assetVersionV0 := asset.V0 + + const numAssets = 1 + assetGen := newAssetGenerator(t, numAssets, 1) + assetGen.genAssets(t, assetsStore, []assetDesc{ + { + assetGen: assetGen.assetGens[0], + anchorPoint: assetGen.anchorPoints[0], + + // This is the script key of the asset we'll be + // modifying. + scriptKey: &targetScriptKey, + + amt: 16, + assetVersion: &assetVersionV0, + }, + }) + + // Formulate a spend delta outbound parcel. This parcel will be stored + // in the database. We will then manipulate the proof delivery status + // of the first transfer output. + // + // First, we'll generate a new anchor transaction for use in the parcel. + newAnchorTx := wire.NewMsgTx(2) + newAnchorTx.AddTxIn(&wire.TxIn{}) + newAnchorTx.TxIn[0].SignatureScript = []byte{} + newAnchorTx.AddTxOut(&wire.TxOut{ + PkScript: bytes.Repeat([]byte{0x01}, 34), + Value: 1000, + }) + anchorTxHash := newAnchorTx.TxHash() + + // Next, we'll generate script keys for the two transfer outputs. + newScriptKey := asset.NewScriptKeyBip86(keychain.KeyDescriptor{ + PubKey: test.RandPubKey(t), + KeyLocator: keychain.KeyLocator{ + Index: uint32(rand.Int31()), + Family: keychain.KeyFamily(rand.Int31()), + }, + }) + + newScriptKey2 := asset.NewScriptKeyBip86(keychain.KeyDescriptor{ + PubKey: test.RandPubKey(t), + KeyLocator: keychain.KeyLocator{ + Index: uint32(rand.Int31()), + Family: keychain.KeyFamily(rand.Int31()), + }, + }) + + // The outbound parcel will split the asset into two outputs. The first + // will have an amount of 9, and the second will have the remainder of + // the asset amount. + newAmt := 9 + + senderBlob := bytes.Repeat([]byte{0x01}, 100) + receiverBlob := bytes.Repeat([]byte{0x02}, 100) + + newWitness := asset.Witness{ + PrevID: &asset.PrevID{}, + TxWitness: [][]byte{{0x01}, {0x02}}, + SplitCommitment: nil, + } + + // Mock proof courier address. + proofCourierAddrBytes := []byte("universerpc://localhost:10009") + + // Fetch the asset that was previously generated. + allAssets, err := assetsStore.FetchAllAssets(ctx, true, false, nil) + require.NoError(t, err) + require.Len(t, allAssets, numAssets) + + inputAsset := allAssets[0] + + // Construct the outbound parcel that will be stored in the database. + spendDelta := &tapfreighter.OutboundParcel{ + AnchorTx: newAnchorTx, + AnchorTxHeightHint: 1450, + ChainFees: int64(100), + Inputs: []tapfreighter.TransferInput{{ + PrevID: asset.PrevID{ + OutPoint: wire.OutPoint{ + Hash: assetGen.anchorTxs[0].TxHash(), + Index: 0, + }, + ID: inputAsset.ID(), + ScriptKey: asset.ToSerialized( + inputAsset.ScriptKey.PubKey, + ), + }, + Amount: inputAsset.Amount, + }}, + Outputs: []tapfreighter.TransferOutput{{ + Anchor: tapfreighter.Anchor{ + Value: 1000, + OutPoint: wire.OutPoint{ + Hash: anchorTxHash, + Index: 0, + }, + InternalKey: keychain.KeyDescriptor{ + PubKey: test.RandPubKey(t), + KeyLocator: keychain.KeyLocator{ + Family: keychain.KeyFamily( + rand.Int31(), + ), + Index: uint32( + test.RandInt[int32](), + ), + }, + }, + TaprootAssetRoot: bytes.Repeat([]byte{0x1}, 32), + MerkleRoot: bytes.Repeat([]byte{0x1}, 32), + }, + ScriptKey: newScriptKey, + ScriptKeyLocal: false, + Amount: uint64(newAmt), + LockTime: 1337, + RelativeLockTime: 31337, + WitnessData: []asset.Witness{newWitness}, + SplitCommitmentRoot: nil, + AssetVersion: asset.V0, + ProofSuffix: receiverBlob, + ProofCourierAddr: proofCourierAddrBytes, + ProofDeliveryComplete: fn.Some[bool](false), + Position: 0, + }, { + Anchor: tapfreighter.Anchor{ + Value: 1000, + OutPoint: wire.OutPoint{ + Hash: anchorTxHash, + Index: 1, + }, + InternalKey: keychain.KeyDescriptor{ + PubKey: test.RandPubKey(t), + KeyLocator: keychain.KeyLocator{ + Family: keychain.KeyFamily( + rand.Int31(), + ), + Index: uint32( + test.RandInt[int32](), + ), + }, + }, + TaprootAssetRoot: bytes.Repeat([]byte{0x1}, 32), + MerkleRoot: bytes.Repeat([]byte{0x1}, 32), + }, + ScriptKey: newScriptKey2, + ScriptKeyLocal: true, + Amount: inputAsset.Amount - uint64(newAmt), + WitnessData: []asset.Witness{newWitness}, + SplitCommitmentRoot: nil, + AssetVersion: asset.V1, + ProofSuffix: senderBlob, + Position: 1, + }}, + } + + // Store the outbound parcel in the database. + leaseOwner := fn.ToArray[[32]byte](test.RandBytes(32)) + leaseExpiry := time.Now().Add(time.Hour) + require.NoError(t, assetsStore.LogPendingParcel( + ctx, spendDelta, leaseOwner, leaseExpiry, + )) + + // At this point, we should be able to query for the log parcel, by + // looking for all unconfirmed transfers. + assetTransfers, err := db.QueryAssetTransfers(ctx, TransferQuery{}) + require.NoError(t, err) + require.Len(t, assetTransfers, 1) + + // We should also be able to find the transfer outputs. + transferOutputs, err := db.FetchTransferOutputs( + ctx, assetTransfers[0].ID, + ) + require.NoError(t, err) + require.Len(t, transferOutputs, 2) + + // Let's confirm that the proof has not been delivered for the first + // transfer output and that the proof delivery status for the second + // transfer output is still unset. + require.Equal( + t, sqlBool(false), transferOutputs[0].ProofDeliveryComplete, + ) + require.Equal( + t, sql.NullBool{}, transferOutputs[1].ProofDeliveryComplete, + ) + + // We will now set the status of the transfer output proof to + // "delivered". + // + // nolint: lll + err = db.SetTransferOutputProofDeliveryStatus( + ctx, OutputProofDeliveryStatus{ + DeliveryComplete: sqlBool(true), + SerializedAnchorOutpoint: transferOutputs[0].AnchorOutpoint, + Position: transferOutputs[0].Position, + }, + ) + require.NoError(t, err) + + // We will check to ensure that the transfer output proof delivery + // status has been updated correctly. + transferOutputs, err = db.FetchTransferOutputs( + ctx, assetTransfers[0].ID, + ) + require.NoError(t, err) + require.Len(t, transferOutputs, 2) + + // The proof delivery status of the first output should be set to + // delivered (true). + require.Equal( + t, sqlBool(true), transferOutputs[0].ProofDeliveryComplete, + ) + + // The proof delivery status of the second output should be unset. + require.Equal( + t, sql.NullBool{}, transferOutputs[1].ProofDeliveryComplete, + ) +}