Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
2e3570e
feat(tbtc): UTXO reservation wallet-side foundations
mswilkison Aug 7, 2026
77fca94
fix(tbtcpg): align GetRedemptionParameters call with struct return
mswilkison Aug 7, 2026
7826468
fix(tbtc): validate reservation proposal payloads
mswilkison Aug 9, 2026
62b18c1
fix(tbtc): bind reservations to action generations
mswilkison Aug 9, 2026
b4f6394
fix(tbtc): bind dissolution inputs to action snapshot
mswilkison Aug 9, 2026
434ba9d
fix(tbtc): harden reservation lane validation and wire redeemer hash …
piotr-roslaniec Sep 1, 2026
9fd8dd3
test(tbtc): assert wallet action metric names cover all types
piotr-roslaniec Sep 1, 2026
3cfd3bc
chore: gitignore agent review workspace
piotr-roslaniec Sep 1, 2026
55c6a0d
fix(tbtcpg): implement Chain reservation methods on LocalChain test d…
piotr-roslaniec Sep 1, 2026
dc24db3
fix(tbtc): gate anchor assembly on action type and settlement state
piotr-roslaniec Sep 1, 2026
4a0678d
fix(tbtc): enforce whole-redemption lineage and reservation wallet bi…
piotr-roslaniec Sep 1, 2026
3416be9
fix(tbtc): standardize reservation chain interface conventions
piotr-roslaniec Sep 1, 2026
f932313
fix(tbtc): dedupe reservation proposal marshaling validation
piotr-roslaniec Sep 1, 2026
db0115d
chore(tbtc): drop unrelated gitignore hunk and derive action-type tes…
piotr-roslaniec Sep 1, 2026
0297289
style(tbtc): gofmt pkg/chain/ethereum/tbtc_test.go and pkg/tbtc/marsh…
piotr-roslaniec Sep 1, 2026
985cd98
fix(tbtc): address review findings on UTXO reservation assemblers
piotr-roslaniec Sep 1, 2026
d5b83e8
fix(tbtc): restore GetDepositMinAge dropped while completing stub doc…
piotr-roslaniec Sep 1, 2026
76baf17
fix(tbtc): prevent reservation anchor from deadlocking wallet sync
piotr-roslaniec Sep 2, 2026
8037b30
fix(tbtc): close reservation action validation gaps and add test cove…
piotr-roslaniec Sep 2, 2026
e9e77aa
fix(tbtc): reject malformed reservation proposal JSON
piotr-roslaniec Sep 2, 2026
e72dea8
docs(tbtc): clarify reservation parameter and stub doc comments
piotr-roslaniec Sep 2, 2026
0577a8f
style(tbtc): gofmt reservation.go and reservation_test.go
piotr-roslaniec Sep 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 118 additions & 20 deletions pkg/chain/ethereum/tbtc.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ const (
EcdsaDkgValidatorContractName = "EcdsaDkgValidator"
)

var errReservationsUnsupported = errors.New("reservations not supported yet by the Ethereum chain implementation")

const (
sweptDepositsCachePeriod = 7 * 24 * time.Hour
)
Expand Down Expand Up @@ -1545,6 +1547,46 @@ func (tc *TbtcChain) ComputeMainUtxoHash(
return computeMainUtxoHash(mainUtxo)
}

// ComputeReservationRedeemerOutputScriptHash computes the keccak256 hash of the
// length-prefixed redeemer output script, as required by the on-chain Bridge
// rules. See also redeemerOutputScriptHash.
func (tc *TbtcChain) ComputeReservationRedeemerOutputScriptHash(
redeemerOutputScript bitcoin.Script,
) ([32]byte, error) {
return redeemerOutputScriptHash(redeemerOutputScript)
}

// redeemerOutputScriptHash computes the keccak256 hash of the length-prefixed
// redeemer output script, as required by the on-chain Bridge rules. It is a
// building block for both the legacy redemption key (see buildRedemptionKey)
// and reservation redemption authorization (see
// ComputeReservationRedeemerOutputScriptHash).
func redeemerOutputScriptHash(redeemerOutputScript bitcoin.Script) ([32]byte, error) {
prefixedRedeemerOutputScript, err := redeemerOutputScript.ToVarLenData()
if err != nil {
return [32]byte{}, fmt.Errorf("cannot build prefixed redeemer output script: [%v]", err)
}
return crypto.Keccak256Hash(prefixedRedeemerOutputScript), nil
}

// buildRedemptionKey builds the redemption key by hashing the concatenation
// of the redeemer output script hash and the wallet public key hash.
func buildRedemptionKey(
walletPublicKeyHash [20]byte,
redeemerOutputScript bitcoin.Script,
) (*big.Int, error) {
redeemerOutputScriptHash, err := redeemerOutputScriptHash(redeemerOutputScript)
if err != nil {
return nil, fmt.Errorf("cannot compute redeemer output script hash: [%v]", err)
}

redemptionKey := crypto.Keccak256Hash(
append(redeemerOutputScriptHash[:], walletPublicKeyHash[:]...),
)

return redemptionKey.Big(), nil
}

func computeMainUtxoHash(mainUtxo *bitcoin.UnspentTransactionOutput) [32]byte {
outputIndexBytes := make([]byte, 4)
binary.BigEndian.PutUint32(outputIndexBytes, mainUtxo.Outpoint.OutputIndex)
Expand Down Expand Up @@ -1698,26 +1740,6 @@ func (tc *TbtcChain) SubmitRedemptionProofWithReimbursement(
return err
}

func buildRedemptionKey(
walletPublicKeyHash [20]byte,
redeemerOutputScript bitcoin.Script,
) (*big.Int, error) {
// The Bridge contract builds the redemption key using the length-prefixed
// redeemer output script.
prefixedRedeemerOutputScript, err := redeemerOutputScript.ToVarLenData()
if err != nil {
return nil, fmt.Errorf("cannot build prefixed redeemer output script: [%v]", err)
}

redeemerOutputScriptHash := crypto.Keccak256Hash(prefixedRedeemerOutputScript)

redemptionKey := crypto.Keccak256Hash(
append(redeemerOutputScriptHash[:], walletPublicKeyHash[:]...),
)

return redemptionKey.Big(), nil
}

func (tc *TbtcChain) TxProofDifficultyFactor() (*big.Int, error) {
return tc.bridge.TxProofDifficultyFactor()
}
Expand Down Expand Up @@ -2408,3 +2430,79 @@ func (tc *TbtcChain) GetRedemptionDelay(
func (tc *TbtcChain) GetDepositMinAge() (uint32, error) {
return tc.walletProposalValidator.DEPOSITMINAGE()
}

// GetReservation returns errReservationsUnsupported because the generated
// Ethereum bindings do not yet expose the reservation Bridge API.
func (tc *TbtcChain) GetReservation(
reservationKey *big.Int,
) (*tbtc.Reservation, bool, error) {
return nil, false, errReservationsUnsupported
}

// GetReservationAction returns errReservationsUnsupported because the
// generated Ethereum bindings do not yet expose the reservation Bridge API.
func (tc *TbtcChain) GetReservationAction(
reservationKey *big.Int,
requestNonce uint64,
) (*tbtc.ReservationAction, error) {
return nil, errReservationsUnsupported
}

// GetReservationParameters returns errReservationsUnsupported because the
// generated Ethereum bindings do not yet expose the reservation Bridge API.
func (tc *TbtcChain) GetReservationParameters() (
tbtc.ReservationParameters,
error,
) {
return tbtc.ReservationParameters{}, errReservationsUnsupported
}

// GetReservationTotalAmount returns errReservationsUnsupported because the
// generated Ethereum bindings do not yet expose the reservation Bridge API.
func (tc *TbtcChain) GetReservationTotalAmount() (uint64, error) {
return 0, errReservationsUnsupported
}

// ValidateReservationAnchorProposal returns errReservationsUnsupported
// because the generated Ethereum bindings do not yet expose the reservation
// Bridge API.
func (tc *TbtcChain) ValidateReservationAnchorProposal(
walletPublicKeyHash [20]byte,
proposal *tbtc.ReservationAnchorProposal,
depositExtraInfo struct {
*tbtc.Deposit
FundingTx *bitcoin.Transaction
},
) error {
return errReservationsUnsupported
}

// ValidateReservedRedemptionProposal returns errReservationsUnsupported
// because the generated Ethereum bindings do not yet expose the reservation
// Bridge API.
func (tc *TbtcChain) ValidateReservedRedemptionProposal(
walletPublicKeyHash [20]byte,
proposal *tbtc.ReservedRedemptionProposal,
) error {
return errReservationsUnsupported
}

// ValidateReservationReanchorProposal returns errReservationsUnsupported
// because the generated Ethereum bindings do not yet expose the reservation
// Bridge API.
func (tc *TbtcChain) ValidateReservationReanchorProposal(
sourceWalletPublicKeyHash [20]byte,
proposal *tbtc.ReservationReanchorProposal,
) error {
return errReservationsUnsupported
}

// ValidateReservationDissolutionProposal returns errReservationsUnsupported
// because the generated Ethereum bindings do not yet expose the reservation
// Bridge API.
func (tc *TbtcChain) ValidateReservationDissolutionProposal(
walletPublicKeyHash [20]byte,
proposal *tbtc.ReservationDissolutionProposal,
) error {
return errReservationsUnsupported
}
70 changes: 69 additions & 1 deletion pkg/chain/ethereum/tbtc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,17 @@ import (
"bytes"
"crypto/ecdsa"
"encoding/hex"
"errors"
"fmt"
"math/big"
"reflect"
"testing"

"github.com/keep-network/keep-core/pkg/bitcoin"

"github.com/keep-network/keep-core/pkg/chain"

"github.com/keep-network/keep-core/pkg/tbtc"

"github.com/ethereum/go-ethereum/common"

"github.com/keep-network/keep-core/internal/testutils"
Expand Down Expand Up @@ -533,3 +535,69 @@ func TestBuildMovedFundsKey(t *testing.T) {
movedFundsKey.Text(16),
)
}

func TestTbtcChainReservationStubs(t *testing.T) {
tc := &TbtcChain{}

reservationKey := big.NewInt(1)
_, _, err := tc.GetReservation(reservationKey)
if !errors.Is(err, errReservationsUnsupported) {
t.Errorf("GetReservation: expected errReservationsUnsupported, got %v", err)
}

_, err = tc.GetReservationAction(reservationKey, 1)
if !errors.Is(err, errReservationsUnsupported) {
t.Errorf("GetReservationAction: expected errReservationsUnsupported, got %v", err)
}

_, err = tc.GetReservationParameters()
if !errors.Is(err, errReservationsUnsupported) {
t.Errorf("GetReservationParameters: expected errReservationsUnsupported, got %v", err)
}

_, err = tc.GetReservationTotalAmount()
if !errors.Is(err, errReservationsUnsupported) {
t.Errorf("GetReservationTotalAmount: expected errReservationsUnsupported, got %v", err)
}

var wpkHash [20]byte

err = tc.ValidateReservationAnchorProposal(wpkHash, &tbtc.ReservationAnchorProposal{}, struct {
*tbtc.Deposit
FundingTx *bitcoin.Transaction
}{})
if !errors.Is(err, errReservationsUnsupported) {
t.Errorf("ValidateReservationAnchorProposal: expected errReservationsUnsupported, got %v", err)
}

err = tc.ValidateReservedRedemptionProposal(wpkHash, &tbtc.ReservedRedemptionProposal{})
if !errors.Is(err, errReservationsUnsupported) {
t.Errorf("ValidateReservedRedemptionProposal: expected errReservationsUnsupported, got %v", err)
}

err = tc.ValidateReservationReanchorProposal(wpkHash, &tbtc.ReservationReanchorProposal{})
if !errors.Is(err, errReservationsUnsupported) {
t.Errorf("ValidateReservationReanchorProposal: expected errReservationsUnsupported, got %v", err)
}

err = tc.ValidateReservationDissolutionProposal(wpkHash, &tbtc.ReservationDissolutionProposal{})
if !errors.Is(err, errReservationsUnsupported) {
t.Errorf("ValidateReservationDissolutionProposal: expected errReservationsUnsupported, got %v", err)
}
}

func TestRedeemerOutputScriptHash(t *testing.T) {
// Example script: "76a9144130879211c54df460e484ddf9aac009cb38ee7488ac"
script := bitcoin.Script{0x76, 0xa9, 0x14, 0x41, 0x30, 0x87, 0x92, 0x11, 0xc5, 0x4d, 0xf4, 0x60, 0xe, 0x48, 0x4d, 0xdf, 0xf9, 0xaa, 0xc0, 0x09, 0xcb, 0x38, 0xee, 0x74, 0x88, 0xac}

hash, err := redeemerOutputScriptHash(script)
if err != nil {
t.Fatal(err)
}

// Known hash
expectedHash := "b04b97d4aaec109acf3af12994b0c088d2cbb96d3cdb8cdfba66c5a1cf9ca86f"
if hex.EncodeToString(hash[:]) != expectedHash {
t.Errorf("expected hash %s, got %s", expectedHash, hex.EncodeToString(hash[:]))
}
}
4 changes: 4 additions & 0 deletions pkg/clientinfo/performance.go
Original file line number Diff line number Diff line change
Expand Up @@ -751,5 +751,9 @@ func GetAllWalletActionTypes() []string {
"redemption",
"moving_funds",
"moved_funds_sweep",
"reservation_anchor",
"reserved_redemption",
"reservation_reanchor",
"reservation_dissolution",
}
}
51 changes: 51 additions & 0 deletions pkg/clientinfo/performance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -430,3 +430,54 @@ func TestJoinFailureAndOnChainCountersRegistered(t *testing.T) {
}
}
}

func TestWalletActionMetricsRegistered(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

registry := &Registry{keepclientinfo.NewRegistry(), ctx}
pm := NewPerformanceMetrics(ctx, registry)

expectedActionTypes := []string{
"heartbeat",
"deposit_sweep",
"redemption",
"moving_funds",
"moved_funds_sweep",
"reservation_anchor",
"reserved_redemption",
"reservation_reanchor",
"reservation_dissolution",
}

for _, actionType := range expectedActionTypes {
for _, metricType := range []string{
"total",
"success_total",
"failed_total",
} {
metricName := WalletActionMetricName(actionType, metricType)

pm.countersMutex.RLock()
_, exists := pm.counters[metricName]
pm.countersMutex.RUnlock()
if !exists {
t.Errorf("counter %s should be registered upfront", metricName)
}
}

durationMetricName := WalletActionMetricName(
actionType,
"duration_seconds",
)
pm.histogramsMutex.RLock()
_, exists := pm.histograms[durationMetricName]
pm.histogramsMutex.RUnlock()
if !exists {
t.Errorf(
"histogram %s should be registered upfront",
durationMetricName,
)
}
}
}
Loading
Loading