Skip to content

Commit 665a37d

Browse files
committed
tapfreighter: add new ChainPorter state and connection check
Adds a new state, SendStateStartHandleAddrParcel, to the ChainPorter state machine. This becomes the initial state for address parcels. In this state, preliminary checks are performed, including attempts to connect to each proof courier service. This allows the send process to exit early—before coin selection or anchor transaction broadcast— if proof transfer to the recipient is expected to fail.
1 parent c8ba79f commit 665a37d

File tree

3 files changed

+138
-5
lines changed

3 files changed

+138
-5
lines changed

itest/addrs_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1029,7 +1029,7 @@ func sendAsset(t *harnessTest, sender *tapdHarness,
10291029
var targetScriptKey []byte = nil
10301030
AssertSendEvents(
10311031
t.t, targetScriptKey, sub,
1032-
tapfreighter.SendStateVirtualCommitmentSelect,
1032+
tapfreighter.SendStateStartHandleAddrParcel,
10331033
tapfreighter.SendStateBroadcast,
10341034
)
10351035

tapfreighter/chain_porter.go

Lines changed: 129 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"context"
66
"errors"
77
"fmt"
8+
"net/url"
89
"strings"
910
"sync"
1011
"time"
@@ -1093,13 +1094,138 @@ func (p *ChainPorter) importLocalAddresses(ctx context.Context,
10931094
return nil
10941095
}
10951096

1097+
// pingCourier attempts to establish a connection to the given proof courier
1098+
// address. If the connection is successful, the courier is closed and the
1099+
// function returns nil. If the connection fails, an error is returned.
1100+
// This function is blocking.
1101+
func (p *ChainPorter) pingCourier(ctx context.Context, addr url.URL) error {
1102+
log.Debugf("Attempting to ping proof courier (addr=%s)", addr.String())
1103+
1104+
// Connect to the proof courier service with an eager (non-lazy)
1105+
// connection attempt, blocking until the connection either succeeds,
1106+
// fails, or times out.
1107+
courier, err := p.cfg.ProofCourierDispatcher.NewCourier(
1108+
ctx, &addr, false,
1109+
)
1110+
if err != nil {
1111+
return fmt.Errorf("unable to initiate proof courier "+
1112+
"service handle (addr=%s): %w", addr.String(), err)
1113+
}
1114+
1115+
return courier.Close()
1116+
}
1117+
1118+
// pingProofCouriers performs a blocking connectivity check for each applicable
1119+
// proof courier.
1120+
func (p *ChainPorter) pingProofCouriers(proofCourierAddrs []url.URL) error {
1121+
// Construct minimal set of unique proof couriers to ping.
1122+
var couriers []url.URL
1123+
1124+
for idx := range proofCourierAddrs {
1125+
addr := proofCourierAddrs[idx]
1126+
1127+
// Check if the address is a duplicate (already in the list of
1128+
// couriers).
1129+
for i := range couriers {
1130+
if addr.String() == couriers[i].String() {
1131+
// Skip duplicate addresses.
1132+
continue
1133+
}
1134+
}
1135+
1136+
couriers = append(couriers, addr)
1137+
}
1138+
1139+
// Ping each proof courier in parallel to ensure they are reachable.
1140+
ctx, cancel := p.WithCtxQuit()
1141+
defer cancel()
1142+
instanceErrors, err := fn.ParSliceErrCollect(
1143+
ctx, couriers, p.pingCourier,
1144+
)
1145+
if err != nil {
1146+
return fmt.Errorf("failed execute proof courier(s) parallel "+
1147+
"ping: %w", err)
1148+
}
1149+
1150+
// If any errors occurred while pinging proof couriers, log them all
1151+
// here.
1152+
for idx := range instanceErrors {
1153+
addr := couriers[idx]
1154+
instanceErr := instanceErrors[idx]
1155+
1156+
log.Errorf("Failed to pinging proof courier (addr=%s): %v",
1157+
addr.String(), instanceErr)
1158+
}
1159+
1160+
// If any errors occurred while pinging proof couriers, return an error.
1161+
if len(instanceErrors) > 0 {
1162+
return fmt.Errorf("failed to ping proof courier(s) "+
1163+
"(error_count=%d)", len(instanceErrors))
1164+
}
1165+
1166+
return nil
1167+
}
1168+
1169+
// prelimCheckAddrParcel performs preliminary validation on the given address
1170+
// parcel. These early checks run before any coin locking or transaction
1171+
// broadcasting occurs.
1172+
func (p *ChainPorter) prelimCheckAddrParcel(addrParcel AddressParcel) error {
1173+
// Currently, the only preliminary check is to ensure that the proof
1174+
// couriers are reachable. If the skip flag is set, we skip this
1175+
// check and exit early.
1176+
if addrParcel.skipProofCourierPingCheck {
1177+
log.Debugf("Flag skipProofCourierPingCheck activated. " +
1178+
"Skipping check. ")
1179+
return nil
1180+
}
1181+
1182+
// Ping the proof couriers to verify that they are reachable.
1183+
// This early check ensures a proof can be reliably delivered
1184+
// to the counterparty before broadcasting a transaction or
1185+
// locking local funds.
1186+
var proofCourierAddrs []url.URL
1187+
for idx := range addrParcel.destAddrs {
1188+
tapAddr := addrParcel.destAddrs[idx]
1189+
1190+
proofCourierAddrs = append(
1191+
proofCourierAddrs, tapAddr.ProofCourierAddr,
1192+
)
1193+
}
1194+
1195+
err := p.pingProofCouriers(proofCourierAddrs)
1196+
if err != nil {
1197+
return fmt.Errorf("failed proof courier(s) connection "+
1198+
"check: %w", err)
1199+
}
1200+
1201+
return nil
1202+
}
1203+
10961204
// stateStep attempts to step through the state machine to complete a Taproot
10971205
// Asset transfer.
10981206
func (p *ChainPorter) stateStep(currentPkg sendPackage) (*sendPackage, error) {
10991207
switch currentPkg.SendState {
1100-
// At this point we have the initial package information populated, so
1101-
// we'll perform coin selection to see if the send request is even
1102-
// possible at all.
1208+
// The initial state entered when the state machine begins processing a
1209+
// new address parcel. In this state, basic validation is performed,
1210+
// such as verifying connectivity to any required proof courier service.
1211+
case SendStateStartHandleAddrParcel:
1212+
// Ensure that the parcel is a valid address parcel.
1213+
addrParcel, ok := currentPkg.Parcel.(*AddressParcel)
1214+
if !ok {
1215+
return nil, fmt.Errorf("unable to cast parcel to " +
1216+
"address parcel")
1217+
}
1218+
1219+
err := p.prelimCheckAddrParcel(*addrParcel)
1220+
if err != nil {
1221+
return nil, fmt.Errorf("failed to perform prelim "+
1222+
"checks on address parcel: %w", err)
1223+
}
1224+
1225+
currentPkg.SendState = SendStateVirtualCommitmentSelect
1226+
return &currentPkg, nil
1227+
1228+
// Perform coin selection for the address parcel.
11031229
case SendStateVirtualCommitmentSelect:
11041230
ctx, cancel := p.WithCtxQuitNoTimeout()
11051231
defer cancel()

tapfreighter/parcel.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,13 @@ import (
2424
type SendState uint8
2525

2626
const (
27+
// SendStateStartHandleAddrParcel is the initial state entered when
28+
// the state machine begins processing a new address parcel.
29+
SendStateStartHandleAddrParcel SendState = iota
30+
2731
// SendStateVirtualCommitmentSelect is the state for performing input
2832
// coin selection to pick out which assets inputs should be spent.
29-
SendStateVirtualCommitmentSelect SendState = iota
33+
SendStateVirtualCommitmentSelect
3034

3135
// SendStateVirtualSign is used to generate the Taproot Asset level
3236
// witness data for any inputs being spent.
@@ -69,6 +73,9 @@ const (
6973
// String returns a human-readable version of SendState.
7074
func (s SendState) String() string {
7175
switch s {
76+
case SendStateStartHandleAddrParcel:
77+
return "SendStateStartHandleAddrParcel"
78+
7279
case SendStateVirtualCommitmentSelect:
7380
return "SendStateVirtualCommitmentSelect"
7481

0 commit comments

Comments
 (0)