Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
14 changes: 12 additions & 2 deletions pkg/chain/ethereum/tbtc.go
Original file line number Diff line number Diff line change
Expand Up @@ -2344,16 +2344,26 @@ func resolveWalletPublicKeyHashForWalletID(
func (tc *TbtcChain) OnWalletClosed(
handler func(event *tbtc.WalletClosedEvent),
) subscription.EventSubscription {
onEvent := func(
onEcdsaEvent := func(
walletID [32]byte,
blockNumber uint64,
) {
handler(&tbtc.WalletClosedEvent{
WalletID: walletID,
Scheme: tbtc.WalletSchemeECDSA,
BlockNumber: blockNumber,
})
}
return tc.walletRegistry.WalletClosedEvent(nil, nil).OnEvent(onEvent)

ecdsaSubscription := tc.walletRegistry.WalletClosedEvent(nil, nil).OnEvent(
onEcdsaEvent,
)
frostSubscription := tc.onFrostWalletClosed(handler)

return subscription.NewEventSubscription(func() {
ecdsaSubscription.Unsubscribe()
frostSubscription.Unsubscribe()
})
}

func (tc *TbtcChain) ComputeMainUtxoHash(
Expand Down
13 changes: 7 additions & 6 deletions pkg/chain/ethereum/tbtc_sortition_chain_views.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ import (
// views bind the sortition.Chain surface to one specific pool/registry with NO
// hasFrostAuthorization() switch, so the caller can run one MonitorPool per pool.
//
// They are used ONLY by the pool-monitoring loops; TbtcChain's own
// sortition.Chain methods (consumed by the heartbeat and other callers) are left
// unchanged, so this introduces no behavior change outside the monitor loops.
// The pool-monitoring loops use these views directly. Wallet heartbeat actions
// also select the view matching the executing wallet, so a process-wide FROST
// configuration cannot redirect a draining legacy wallet's authorization check.
// GetOperatorID stays bound to whichever pool the view targets, matching the
// per-pool member IDs (the legacy view's GetOperatorID equals TbtcChain's
// deliberately-ECDSA-bound GetOperatorID).
Expand Down Expand Up @@ -214,8 +214,9 @@ func (c *frostSortitionChain) UpdateOperatorStatus() error {
}

// LegacyECDSASortitionChain returns a sortition.Chain bound explicitly to the
// legacy ECDSA sortition pool, for ECDSA pool monitoring during the FROST
// migration drain (independent of whether FROST authorization is configured).
// legacy ECDSA sortition pool, for pool monitoring and legacy wallet actions
// during the FROST migration drain (independent of whether FROST authorization
// is configured).
func (tc *TbtcChain) LegacyECDSASortitionChain() sortition.Chain {
return &ecdsaSortitionChain{
sortitionPoolView: sortitionPoolView{
Expand All @@ -229,7 +230,7 @@ func (tc *TbtcChain) LegacyECDSASortitionChain() sortition.Chain {
// FrostSortitionChain returns a sortition.Chain bound explicitly to the FROST
// sortition pool, and a flag reporting whether FROST authorization is configured
// for this node. When the flag is false, the returned chain is nil and FROST
// pool monitoring must not be started.
// pool monitoring or wallet actions must not be started.
func (tc *TbtcChain) FrostSortitionChain() (sortition.Chain, bool) {
if !tc.hasFrostAuthorization() {
return nil, false
Expand Down
188 changes: 188 additions & 0 deletions pkg/chain/ethereum/tbtc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package ethereum

import (
"bytes"
"context"
"crypto/ecdsa"
"encoding/hex"
"errors"
Expand Down Expand Up @@ -287,6 +288,193 @@ func TestCalculateInactivityClaimHash(t *testing.T) {
)
}

func TestCalculateFrostInactivityClaimHash(t *testing.T) {
chainID := big.NewInt(31337)
nonce := big.NewInt(3)

xOnlyOutputKeyBytes, err := hex.DecodeString(
"9a0544440cc47779235ccb76d669590c2cd20c7e431f97e17a1093faf03291c4",
)
if err != nil {
t.Fatal(err)
}
var xOnlyOutputKey [32]byte
copy(xOnlyOutputKey[:], xOnlyOutputKeyBytes)

inactiveMembersIndexes := []*big.Int{
big.NewInt(1), big.NewInt(2), big.NewInt(30),
}

hash, err := calculateFrostInactivityClaimHash(
chainID,
nonce,
xOnlyOutputKey,
inactiveMembersIndexes,
true,
)
if err != nil {
t.Fatal(err)
}

// Vector generated from FrostInactivity.verifyClaim's exact Solidity
// encoding: abi.encode(uint256,uint256,bytes32,uint256[],bool). The bytes32
// key is static; using the legacy dynamic bytes public key changes this hash.
expectedHash := "4e42a992e062421b59b57f6904544d68d65b6a8434a2d579b3d1d74a3f1a0b59"

testutils.AssertStringsEqual(
t,
"FROST inactivity hash",
expectedHash,
hex.EncodeToString(hash[:]),
)
}

func TestInactivityClaimChainForWalletBindsRegistryAndPool(t *testing.T) {
tc, ecdsaPool, frostPool, _ := newTbtcChainForSortitionViewTest(true)

legacyView, err := tc.InactivityClaimChainForWallet(tbtcpkg.WalletSchemeECDSA)
if err != nil {
t.Fatal(err)
}
if legacyView != tc {
t.Fatal("legacy inactivity view must remain bound to TbtcChain")
}
if tc.sortitionPool != ecdsaPool {
t.Fatal("legacy inactivity view must use the ECDSA operator ID pool")
}

frostView, err := tc.InactivityClaimChainForWallet(tbtcpkg.WalletSchemeFROST)
if err != nil {
t.Fatal(err)
}
typedFrostView, ok := frostView.(*frostInactivityClaimChain)
if !ok {
t.Fatalf("expected *frostInactivityClaimChain, got [%T]", frostView)
}
if typedFrostView.TbtcChain != tc {
t.Fatal("FROST inactivity view must reference the owning chain")
}
if typedFrostView.frostSortitionPool != frostPool {
t.Fatal("FROST inactivity view must use the FROST operator ID pool")
}
if typedFrostView.frostSortitionPool == ecdsaPool {
t.Fatal("FROST inactivity view must not use the ECDSA operator ID pool")
}
}

func TestConvertFrostWalletClosedEventMarksFrostScheme(t *testing.T) {
walletID := [32]byte{0xaa, 0xbb, 0xcc}
event := convertFrostWalletClosedEvent(
&frostabi.FrostWalletRegistryWalletClosed{
WalletID: walletID,
Raw: types.Log{
BlockNumber: 123,
},
},
)

if event.WalletID != walletID {
t.Fatalf("unexpected wallet ID [0x%x]", event.WalletID)
}
if event.Scheme != tbtcpkg.WalletSchemeFROST {
t.Fatalf("unexpected wallet scheme [%v]", event.Scheme)
}
if event.BlockNumber != 123 {
t.Fatalf("unexpected block number [%v]", event.BlockNumber)
}
}

func TestConvertFrostWalletClosedEventRejectsNil(t *testing.T) {
if event := convertFrostWalletClosedEvent(nil); event != nil {
t.Fatalf("expected nil event, got [%+v]", event)
}
}

func TestHandleFrostWalletClosedEventsStopsAfterCancellation(t *testing.T) {
ctx, cancelCtx := context.WithCancel(context.Background())
events := make(chan *tbtcpkg.WalletClosedEvent, 1)
events <- &tbtcpkg.WalletClosedEvent{WalletID: [32]byte{0xaa}}
cancelCtx()

handled := false
handleFrostWalletClosedEvents(
ctx,
events,
func(event *tbtcpkg.WalletClosedEvent) {
handled = true
},
)

if handled {
t.Fatal("handler invoked after cancellation")
}
}

func TestConvertFrostInactivityClaimedEvent(t *testing.T) {
walletID := [32]byte{0xaa, 0xbb, 0xcc}
nonce := big.NewInt(17)
notifier := common.HexToAddress("0x1234567890123456789012345678901234567890")

event := convertFrostInactivityClaimedEvent(
&frostabi.FrostWalletRegistryInactivityClaimed{
WalletID: walletID,
Nonce: nonce,
Notifier: notifier,
Raw: types.Log{
BlockNumber: 123,
},
},
)

if event.WalletID != walletID {
t.Fatalf("unexpected wallet ID [0x%x]", event.WalletID)
}
if event.Nonce.Cmp(nonce) != 0 {
t.Fatalf("unexpected nonce [%v]", event.Nonce)
}
if event.Notifier != chain.Address(notifier.Hex()) {
t.Fatalf("unexpected notifier [%v]", event.Notifier)
}
if event.BlockNumber != 123 {
t.Fatalf("unexpected block number [%v]", event.BlockNumber)
}
}

func TestConvertFrostInactivityClaimedEventRejectsNil(t *testing.T) {
if event := convertFrostInactivityClaimedEvent(nil); event != nil {
t.Fatalf("expected nil event, got [%+v]", event)
}
}

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

done := make(chan struct{})
go func() {
emitFrostInactivityClaimedEvent(
ctx,
make(chan *tbtcpkg.InactivityClaimedEvent),
&tbtcpkg.InactivityClaimedEvent{},
)
close(done)
}()

<-done
}

func TestFrostInactivityClaimChainRejectsNilInputs(t *testing.T) {
if _, err := convertFrostInactivityClaimToABI(nil); err == nil {
t.Fatal("expected nil inactivity claim error")
}

view := &frostInactivityClaimChain{}
err := view.SubmitInactivityClaim(&tbtcpkg.InactivityClaim{}, nil, nil)
if err == nil || err.Error() != "inactivity claim nonce is nil" {
t.Fatalf("unexpected nil nonce error: [%v]", err)
}
}

func TestCalculateWalletID(t *testing.T) {
hexToByte32 := func(hexStr string) [32]byte {
if len(hexStr) != 64 {
Expand Down
Loading
Loading