Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
f6b15b3
fix(tecdsa): TOB-TBTCACEXT-11 — count the third operator's seats in t…
lrsaturnino Jul 1, 2026
0841959
fix(ethereum): TOB-TBTCACEXT-10 — read TxMaxFee from its own field in…
lrsaturnino Jul 1, 2026
1152b4c
fix(tbtc): TOB-TBTCACEXT-5 — guard doneSigners map on every access in…
lrsaturnino Jul 1, 2026
b2231e5
fix(retransmission): TOB-TBTCACEXT-6 — synchronize BackoffStrategy ti…
lrsaturnino Jul 1, 2026
b983c6c
fix(tbtc): TOB-TBTCACEXT-26 — reject signing-done messages from non-a…
lrsaturnino Jul 1, 2026
79df7b2
fix(tbtc): TOB-TBTCACEXT-70 — reject non-canonical high-S signer appr…
lrsaturnino Jul 1, 2026
00af4e2
fix(tbtc): TOB-TBTCACEXT-2 — derive P2WSH output script for migration…
lrsaturnino Jul 1, 2026
ae45f8b
fix(covenantsigner): TOB-TBTCACEXT-65 — use a flat jobs directory so …
lrsaturnino Jul 1, 2026
6aad159
fix(tbtc): TOB-TBTCACEXT-87 — deauthorize archived and non-live walle…
lrsaturnino Jul 1, 2026
fdc568e
fix(covenantsigner): TOB-TBTCACEXT-92 — fail production signer startu…
lrsaturnino Jul 1, 2026
47ac3c4
fix(tbtc): TOB-TBTCACEXT-36 — do not archive wallets that are pending…
lrsaturnino Jul 1, 2026
a462df3
fix(escrow): TOB-TBTCACEXT-27 — accumulate the withdrawn counter on r…
lrsaturnino Jul 1, 2026
4336e54
fix(tbtc): TOB-TBTCACEXT-56 — deduplicate DKG results only after hand…
lrsaturnino Jul 1, 2026
20c2b76
fix(tbtc): TOB-TBTCACEXT-57 — deduplicate wallet closures only after …
lrsaturnino Jul 1, 2026
4c3f4a1
fix(tbtc): TOB-TBTCACEXT-72 — deduplicate DKG started events only aft…
lrsaturnino Jul 1, 2026
21c5297
fix(spv): TOB-TBTCACEXT-22 — scan past spam when discovering SPV proo…
lrsaturnino Jul 1, 2026
276547b
fix(tbtc): TOB-TBTCACEXT-1 — fail closed on covenant signing without …
lrsaturnino Jul 1, 2026
e688ba4
Merge feat/psbt-covenant-final-project-pr into fix/tob-tbtcacext-reme…
lrsaturnino Jul 1, 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
6 changes: 6 additions & 0 deletions cmd/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,12 @@ func initCovenantSignerFlags(cmd *cobra.Command, cfg *config.Config) {
false,
"Fail startup when enabled covenant routes are missing route-level approval trust roots. Request-time validation still enforces exact reserve/network trust-root matches.",
)
cmd.Flags().BoolVar(
&cfg.CovenantSigner.BridgeCovenantFraudDefenseConfirmed,
"covenantSigner.bridgeCovenantFraudDefenseConfirmed",
false,
"Set only after confirming the tBTC Bridge covenant fraud-defense path is deployed. Until set, the covenant signer fails closed and refuses to sign, because a covenant signature would otherwise expose the wallet to an undefeatable fraud challenge.",
)
}

// Initialize flags for Maintainer configuration.
Expand Down
7 changes: 7 additions & 0 deletions cmd/flags_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,13 @@ var cmdFlagsTests = map[string]struct {
expectedValueFromFlag: true,
defaultValue: false,
},
"covenantSigner.bridgeCovenantFraudDefenseConfirmed": {
readValueFunc: func(c *config.Config) interface{} { return c.CovenantSigner.BridgeCovenantFraudDefenseConfirmed },
flagName: "--covenantSigner.bridgeCovenantFraudDefenseConfirmed",
flagValue: "",
expectedValueFromFlag: true,
defaultValue: false,
},
"tbtc.preParamsPoolSize": {
readValueFunc: func(c *config.Config) interface{} { return c.Tbtc.PreParamsPoolSize },
flagName: "--tbtc.preParamsPoolSize",
Expand Down
2 changes: 2 additions & 0 deletions cmd/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ type startDeps struct {
clientInfoRegistry *clientinfo.Registry,
perfMetrics *clientinfo.PerformanceMetrics,
minActiveOutpointConfirmations uint,
bridgeCovenantFraudDefenseConfirmed bool,
) (covenantsigner.Engine, error)
initializeSigner func(
ctx context.Context,
Expand Down Expand Up @@ -254,6 +255,7 @@ func startWithDeps(cmd *cobra.Command, deps startDeps) error {
clientInfoRegistry,
perfMetrics, // Pass the existing performance metrics instance to avoid duplicate registrations
clientConfig.CovenantSigner.MinActiveOutpointConfirmations,
clientConfig.CovenantSigner.BridgeCovenantFraudDefenseConfirmed,
)
if err != nil {
return fmt.Errorf("error initializing TBTC: [%v]", err)
Expand Down
40 changes: 27 additions & 13 deletions pkg/chain/ethereum/tbtc.go
Original file line number Diff line number Diff line change
Expand Up @@ -1251,6 +1251,32 @@ func (tc *TbtcChain) PastDepositRevealedEvents(
return convertedEvents, err
}

// convertPastRedemptionRequestedEvent maps a single on-chain RedemptionRequested
// event to its off-chain tbtc.RedemptionRequestedEvent representation. The event
// carries two distinct fee fields, TreasuryFee and TxMaxFee, that must be mapped
// to their matching destination fields independently; keeping the mapping in a
// standalone helper lets that field-by-field correspondence be unit tested.
func convertPastRedemptionRequestedEvent(
event *tbtcabi.BridgeRedemptionRequested,
) (*tbtc.RedemptionRequestedEvent, error) {
redeemerOutputScript, err := bitcoin.NewScriptFromVarLenData(
event.RedeemerOutputScript,
)
if err != nil {
return nil, err
}

return &tbtc.RedemptionRequestedEvent{
WalletPublicKeyHash: event.WalletPubKeyHash,
RedeemerOutputScript: redeemerOutputScript,
Redeemer: chain.Address(event.Redeemer.Hex()),
RequestedAmount: event.RequestedAmount,
TreasuryFee: event.TreasuryFee,
TxMaxFee: event.TxMaxFee,
BlockNumber: event.Raw.BlockNumber,
}, nil
}

func (tc *TbtcChain) PastRedemptionRequestedEvents(
filter *tbtc.RedemptionRequestedEventFilter,
) ([]*tbtc.RedemptionRequestedEvent, error) {
Expand Down Expand Up @@ -1282,23 +1308,11 @@ func (tc *TbtcChain) PastRedemptionRequestedEvents(

convertedEvents := make([]*tbtc.RedemptionRequestedEvent, 0)
for _, event := range events {
redeemerOutputScript, err := bitcoin.NewScriptFromVarLenData(
event.RedeemerOutputScript,
)
convertedEvent, err := convertPastRedemptionRequestedEvent(event)
if err != nil {
return nil, err
}

convertedEvent := &tbtc.RedemptionRequestedEvent{
WalletPublicKeyHash: event.WalletPubKeyHash,
RedeemerOutputScript: redeemerOutputScript,
Redeemer: chain.Address(event.Redeemer.Hex()),
RequestedAmount: event.RequestedAmount,
TreasuryFee: event.TreasuryFee,
TxMaxFee: event.TreasuryFee,
BlockNumber: event.Raw.BlockNumber,
}

convertedEvents = append(convertedEvents, convertedEvent)
}

Expand Down
86 changes: 86 additions & 0 deletions pkg/chain/ethereum/tbtc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/keep-network/keep-core/pkg/chain"

"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
commonEthereum "github.com/keep-network/keep-common/pkg/chain/ethereum"

"github.com/keep-network/keep-core/internal/testutils"
Expand All @@ -24,6 +25,91 @@ import (
tbtcpkg "github.com/keep-network/keep-core/pkg/tbtc"
)

func TestConvertPastRedemptionRequestedEvent(t *testing.T) {
// RedeemerOutputScript is stored on-chain as variable-length data (a
// CompactSizeUint length prefix followed by the script bytes), so build the
// fixture from a known script to guarantee the helper can parse it back.
redeemerOutputScript := bitcoin.Script([]byte{0x76, 0xa9, 0x14})
redeemerOutputScriptVarLen, err := redeemerOutputScript.ToVarLenData()
if err != nil {
t.Fatal(err)
}

walletPublicKeyHash := [20]byte{1, 2, 3}
redeemer := common.HexToAddress("0x1111111111111111111111111111111111111111")

// The treasury fee and the per-transaction max fee are two independent
// on-chain fields. They are set to distinct values so that a mapping which
// reads the wrong source (e.g. populating TxMaxFee from TreasuryFee) is
// caught rather than masked by equal values.
const (
requestedAmount = uint64(1_000_000)
treasuryFee = uint64(2_000)
txMaxFee = uint64(7_500)
blockNumber = uint64(123_456)
)

event := &tbtcabi.BridgeRedemptionRequested{
WalletPubKeyHash: walletPublicKeyHash,
RedeemerOutputScript: redeemerOutputScriptVarLen,
Redeemer: redeemer,
RequestedAmount: requestedAmount,
TreasuryFee: treasuryFee,
TxMaxFee: txMaxFee,
Raw: types.Log{BlockNumber: blockNumber},
}

converted, err := convertPastRedemptionRequestedEvent(event)
if err != nil {
t.Fatalf("unexpected error: [%v]", err)
}

testutils.AssertUintsEqual(
t,
"requested amount",
requestedAmount,
converted.RequestedAmount,
)

// Both fee fields must be populated from their own distinct source field.
testutils.AssertUintsEqual(
t,
"treasury fee",
treasuryFee,
converted.TreasuryFee,
)
testutils.AssertUintsEqual(
t,
"tx max fee",
txMaxFee,
converted.TxMaxFee,
)

testutils.AssertUintsEqual(
t,
"block number",
blockNumber,
converted.BlockNumber,
)

testutils.AssertBytesEqual(t, redeemerOutputScript, converted.RedeemerOutputScript)

testutils.AssertStringsEqual(
t,
"redeemer",
chain.Address(redeemer.Hex()).String(),
converted.Redeemer.String(),
)

if converted.WalletPublicKeyHash != walletPublicKeyHash {
t.Errorf(
"unexpected wallet public key hash\nexpected: [%x]\nactual: [%x]",
walletPublicKeyHash,
converted.WalletPublicKeyHash,
)
}
}

func TestComputeOperatorsIDsHash(t *testing.T) {
operatorIDs := []chain.OperatorID{
5, 1, 55, 45435534, 33, 345, 23, 235, 3333, 2,
Expand Down
8 changes: 8 additions & 0 deletions pkg/covenantsigner/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@ type Config struct {
// covenant signer accepts it. When zero (unset), the system defaults to 6
// to align with the deposit sweep finality threshold.
MinActiveOutpointConfirmations uint `mapstructure:"minActiveOutpointConfirmations"`
// BridgeCovenantFraudDefenseConfirmed must be set only when the operator has
// confirmed that the tBTC Bridge recognizes covenant active UTXO spends as
// honest spends in Fraud.defeatFraudChallenge (the covenant fraud-defense
// path is deployed). Until then the covenant signer fails closed and refuses
// to produce signatures, because a covenant signature over a covenant active
// UTXO would otherwise be a valid, undefeatable tBTC fraud proof that could
// slash the signing wallet.
BridgeCovenantFraudDefenseConfirmed bool `mapstructure:"bridgeCovenantFraudDefenseConfirmed"`
// DataDir is the base directory path used by the disk persistence handle.
// When set, the store acquires an exclusive file lock to prevent concurrent
// process corruption. When empty, file locking is skipped.
Expand Down
22 changes: 15 additions & 7 deletions pkg/covenantsigner/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@ func Initialize(
listenAddress = DefaultListenAddress
}

if !isLoopbackListenAddress(listenAddress) && strings.TrimSpace(config.AuthToken) == "" {
isLoopback := isLoopbackListenAddress(listenAddress)

if !isLoopback && strings.TrimSpace(config.AuthToken) == "" {
return nil, false, fmt.Errorf(
"covenant signer authToken is required for non-loopback listenAddress [%s]",
listenAddress,
Expand All @@ -66,7 +68,12 @@ func Initialize(
if err != nil {
return nil, false, err
}
if err := validateRequiredApprovalTrustRoots(config, service); err != nil {
// A non-loopback (production) listen address is treated as requiring the
// full multi-party approval model: the signer approval verifier and the
// route trust roots must be configured, mirroring the non-loopback authToken
// requirement above. Loopback deployments may still run with warnings unless
// requireApprovalTrustRoots is set explicitly.
if err := validateRequiredApprovalTrustRoots(config, service, !isLoopback); err != nil {
return nil, false, err
}
if service.signerApprovalVerifier == nil {
Expand Down Expand Up @@ -181,8 +188,9 @@ func Initialize(
func validateRequiredApprovalTrustRoots(
config Config,
service *Service,
requireForNonLoopbackListenAddress bool,
) error {
if !config.RequireApprovalTrustRoots {
if !config.RequireApprovalTrustRoots && !requireForNonLoopbackListenAddress {
return nil
}

Expand All @@ -192,7 +200,7 @@ func validateRequiredApprovalTrustRoots(
TemplateSelfV1,
) {
return fmt.Errorf(
"covenant signer self_v1 routes require depositorTrustRoots when covenantSigner.requireApprovalTrustRoots=true",
"covenant signer self_v1 routes require depositorTrustRoots when covenantSigner.requireApprovalTrustRoots=true or for a non-loopback listen address",
)
}

Expand All @@ -201,7 +209,7 @@ func validateRequiredApprovalTrustRoots(
TemplateQcV1,
) {
return fmt.Errorf(
"covenant signer qc_v1 routes require depositorTrustRoots when covenantSigner.requireApprovalTrustRoots=true",
"covenant signer qc_v1 routes require depositorTrustRoots when covenantSigner.requireApprovalTrustRoots=true or for a non-loopback listen address",
)
}

Expand All @@ -210,13 +218,13 @@ func validateRequiredApprovalTrustRoots(
TemplateQcV1,
) {
return fmt.Errorf(
"covenant signer qc_v1 routes require custodianTrustRoots when covenantSigner.requireApprovalTrustRoots=true",
"covenant signer qc_v1 routes require custodianTrustRoots when covenantSigner.requireApprovalTrustRoots=true or for a non-loopback listen address",
)
}

if service.signerApprovalVerifier == nil {
return fmt.Errorf(
"covenant signer requires a signerApprovalVerifier when covenantSigner.requireApprovalTrustRoots=true",
"covenant signer requires a signerApprovalVerifier when covenantSigner.requireApprovalTrustRoots=true or for a non-loopback listen address",
)
}

Expand Down
61 changes: 61 additions & 0 deletions pkg/covenantsigner/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,67 @@ func TestInitializeRequiresSignerApprovalVerifierWhenConfigured(t *testing.T) {
}
}

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

// A non-loopback (production) listen address must fail startup when the
// required approval trust roots are missing, even without
// RequireApprovalTrustRoots being set explicitly. The engine provides a
// verifier, so the failure is attributable to the missing trust roots.
_, enabled, err := Initialize(
ctx,
Config{
Port: availableLoopbackPort(t),
ListenAddress: "0.0.0.0",
AuthToken: "test-token",
},
handle,
&scriptedVerifierEngine{},
)
if err == nil || enabled {
t.Fatalf("expected non-loopback startup without trust roots to fail, got enabled=%v err=%v", enabled, err)
}
if !strings.Contains(err.Error(), "non-loopback listen address") {
t.Fatalf("unexpected error: %v", err)
}
}

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

// A non-loopback (production) listen address with all trust roots present
// but no signer approval verifier must fail startup.
_, enabled, err := Initialize(
ctx,
Config{
Port: availableLoopbackPort(t),
ListenAddress: "0.0.0.0",
AuthToken: "test-token",
DepositorTrustRoots: []DepositorTrustRoot{
testDepositorTrustRoot(TemplateQcV1),
},
CustodianTrustRoots: []CustodianTrustRoot{
testCustodianTrustRoot(TemplateQcV1),
},
},
handle,
&scriptedEngine{},
)
if err == nil || enabled {
t.Fatalf("expected non-loopback startup without a verifier to fail, got enabled=%v err=%v", enabled, err)
}
if !strings.Contains(err.Error(), "requires a signerApprovalVerifier") {
t.Fatalf("unexpected error: %v", err)
}
if !strings.Contains(err.Error(), "non-loopback listen address") {
t.Fatalf("unexpected error: %v", err)
}
}

func TestIsLoopbackListenAddressAcceptsBracketedIPv6Loopback(t *testing.T) {
if !isLoopbackListenAddress("[::1]") {
t.Fatal("expected bracketed IPv6 loopback address to be recognized")
Expand Down
Loading
Loading