Skip to content

Commit

Permalink
Archive key material on wallet closure (#3814)
Browse files Browse the repository at this point in the history
#Refs: #3797.
This PR handles archiving key material on wallet closures.
Archiving the wallet data happens in two situations:
- when the `WalletClosed` event is received
- on node start, if there are closed wallets in the node's wallet
registry.

Archiving wallet's key material causes the wallet data to be moved to
the `archive` directory and the wallet is removed
form the registry's cache..
Such wallets will no longer participate in the RFC-12 coordination.
  • Loading branch information
lukasz-zimnoch authored Jun 11, 2024
2 parents ea48ec5 + 2743e64 commit efd3f20
Show file tree
Hide file tree
Showing 15 changed files with 864 additions and 29 deletions.
48 changes: 48 additions & 0 deletions pkg/chain/ethereum/tbtc.go
Original file line number Diff line number Diff line change
Expand Up @@ -1415,6 +1415,39 @@ func (tc *TbtcChain) PastNewWalletRegisteredEvents(
return convertedEvents, err
}

func (tc *TbtcChain) CalculateWalletID(
walletPublicKey *ecdsa.PublicKey,
) ([32]byte, error) {
return calculateWalletID(walletPublicKey)
}

func calculateWalletID(walletPublicKey *ecdsa.PublicKey) ([32]byte, error) {
walletPublicKeyBytes, err := convertPubKeyToChainFormat(walletPublicKey)
if err != nil {
return [32]byte{}, fmt.Errorf(
"error while converting wallet public key to chain format: [%v]",
err,
)
}

return crypto.Keccak256Hash(walletPublicKeyBytes[:]), nil
}

func (tc *TbtcChain) IsWalletRegistered(EcdsaWalletID [32]byte) (bool, error) {
isWalletRegistered, err := tc.walletRegistry.IsWalletRegistered(
EcdsaWalletID,
)
if err != nil {
return false, fmt.Errorf(
"cannot check if wallet with ECDSA ID [0x%x] is registered: [%v]",
EcdsaWalletID,
err,
)
}

return isWalletRegistered, nil
}

func (tc *TbtcChain) GetWallet(
walletPublicKeyHash [20]byte,
) (*tbtc.WalletChainData, error) {
Expand Down Expand Up @@ -1453,6 +1486,21 @@ func (tc *TbtcChain) GetWallet(
}, nil
}

func (tc *TbtcChain) OnWalletClosed(
handler func(event *tbtc.WalletClosedEvent),
) subscription.EventSubscription {
onEvent := func(
walletID [32]byte,
blockNumber uint64,
) {
handler(&tbtc.WalletClosedEvent{
WalletID: walletID,
BlockNumber: blockNumber,
})
}
return tc.walletRegistry.WalletClosedEvent(nil, nil).OnEvent(onEvent)
}

func (tc *TbtcChain) ComputeMainUtxoHash(
mainUtxo *bitcoin.UnspentTransactionOutput,
) [32]byte {
Expand Down
44 changes: 44 additions & 0 deletions pkg/chain/ethereum/tbtc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/ethereum/go-ethereum/common"

"github.com/keep-network/keep-core/internal/testutils"
"github.com/keep-network/keep-core/pkg/chain/local_v1"
"github.com/keep-network/keep-core/pkg/protocol/group"
)

Expand Down Expand Up @@ -279,6 +280,49 @@ func TestCalculateInactivityClaimHash(t *testing.T) {
)
}

func TestCalculateWalletID(t *testing.T) {
hexToByte32 := func(hexStr string) [32]byte {
if len(hexStr) != 64 {
t.Fatal("hex string length incorrect")
}

decoded, err := hex.DecodeString(hexStr)
if err != nil {
t.Fatal(err)
}

var result [32]byte
copy(result[:], decoded)

return result
}

xBytes := hexToByte32(
"9a0544440cc47779235ccb76d669590c2cd20c7e431f97e17a1093faf03291c4",
)

yBytes := hexToByte32(
"73e661a208a8a565ca1e384059bd2ff7ff6886df081ff1229250099d388c83df",
)

walletPublicKey := &ecdsa.PublicKey{
Curve: local_v1.DefaultCurve,
X: new(big.Int).SetBytes(xBytes[:]),
Y: new(big.Int).SetBytes(yBytes[:]),
}

actualWalletID, err := calculateWalletID(walletPublicKey)
if err != nil {
t.Fatal(err)
}

expectedWalletID := hexToByte32(
"a6602e554b8cf7c23538fd040e4ff3520ec680e5e5ce9a075259e613a3e5aa79",
)

testutils.AssertBytesEqual(t, expectedWalletID[:], actualWalletID[:])
}

func TestParseDkgResultValidationOutcome(t *testing.T) {
isValid, err := parseDkgResultValidationOutcome(
&struct {
Expand Down
22 changes: 22 additions & 0 deletions pkg/tbtc/chain.go
Original file line number Diff line number Diff line change
Expand Up @@ -235,13 +235,35 @@ type DKGParameters struct {
ApprovePrecedencePeriodBlocks uint64
}

// WalletClosedEvent represents a wallet closed event. It is emitted when the
// wallet is closed in the wallet registry.
type WalletClosedEvent struct {
WalletID [32]byte
BlockNumber uint64
}

// BridgeChain defines the subset of the TBTC chain interface that pertains
// specifically to the tBTC Bridge operations.
type BridgeChain interface {
// CalculateWalletID calculates the wallet's ECDSA ID based on the provided
// wallet public key.
CalculateWalletID(walletPublicKey *ecdsa.PublicKey) ([32]byte, error)

// IsWalletRegistered checks whether the given wallet is registered in the
// ECDSA wallet registry.
IsWalletRegistered(EcdsaWalletID [32]byte) (bool, error)

// GetWallet gets the on-chain data for the given wallet. Returns an error
// if the wallet was not found.
GetWallet(walletPublicKeyHash [20]byte) (*WalletChainData, error)

// OnWalletClosed registers a callback that is invoked when an on-chain
// notification of the wallet closed is seen. The notification occurs when
// the wallet is closed or terminated.
OnWalletClosed(
func(event *WalletClosedEvent),
) subscription.EventSubscription

// ComputeMainUtxoHash computes the hash of the provided main UTXO
// according to the on-chain Bridge rules.
ComputeMainUtxoHash(mainUtxo *bitcoin.UnspentTransactionOutput) [32]byte
Expand Down
61 changes: 59 additions & 2 deletions pkg/tbtc/chain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,17 @@ import (
"sync"
"time"

"golang.org/x/crypto/sha3"

"github.com/ethereum/go-ethereum/crypto"
"github.com/keep-network/keep-core/pkg/bitcoin"
"github.com/keep-network/keep-core/pkg/chain"
"github.com/keep-network/keep-core/pkg/chain/local_v1"
"github.com/keep-network/keep-core/pkg/internal/byteutils"
"github.com/keep-network/keep-core/pkg/operator"
"github.com/keep-network/keep-core/pkg/protocol/group"
"github.com/keep-network/keep-core/pkg/protocol/inactivity"
"github.com/keep-network/keep-core/pkg/subscription"
"github.com/keep-network/keep-core/pkg/tecdsa/dkg"
"golang.org/x/crypto/sha3"
)

const (
Expand Down Expand Up @@ -843,6 +843,40 @@ func buildDepositRequestKey(
return sha256.Sum256(append(fundingTxHash[:], buffer...))
}

func (lc *localChain) CalculateWalletID(
walletPublicKey *ecdsa.PublicKey,
) ([32]byte, error) {
walletPublicKeyBytes, err := convertPubKeyToChainFormat(walletPublicKey)
if err != nil {
return [32]byte{}, fmt.Errorf(
"error while converting wallet public key to chain format: [%v]",
err,
)
}

return crypto.Keccak256Hash(walletPublicKeyBytes[:]), nil
}

func convertPubKeyToChainFormat(publicKey *ecdsa.PublicKey) ([64]byte, error) {
var serialized [64]byte

x, err := byteutils.LeftPadTo32Bytes(publicKey.X.Bytes())
if err != nil {
return serialized, err
}

y, err := byteutils.LeftPadTo32Bytes(publicKey.Y.Bytes())
if err != nil {
return serialized, err
}

serializedBytes := append(x, y...)

copy(serialized[:], serializedBytes)

return serialized, nil
}

func (lc *localChain) GetWallet(walletPublicKeyHash [20]byte) (
*WalletChainData,
error,
Expand All @@ -858,6 +892,23 @@ func (lc *localChain) GetWallet(walletPublicKeyHash [20]byte) (
return walletChainData, nil
}

func (lc *localChain) IsWalletRegistered(EcdsaWalletID [32]byte) (bool, error) {
lc.walletsMutex.Lock()
defer lc.walletsMutex.Unlock()

for _, walletData := range lc.wallets {
if EcdsaWalletID == walletData.EcdsaWalletID {
if walletData.State == StateClosed ||
walletData.State == StateTerminated {
return false, nil
}
return true, nil
}
}

return false, fmt.Errorf("wallet not found")
}

func (lc *localChain) setWallet(
walletPublicKeyHash [20]byte,
walletChainData *WalletChainData,
Expand All @@ -868,6 +919,12 @@ func (lc *localChain) setWallet(
lc.wallets[walletPublicKeyHash] = walletChainData
}

func (lc *localChain) OnWalletClosed(
handler func(event *WalletClosedEvent),
) subscription.EventSubscription {
panic("unsupported")
}

func (lc *localChain) ComputeMainUtxoHash(
mainUtxo *bitcoin.UnspentTransactionOutput,
) [32]byte {
Expand Down
26 changes: 26 additions & 0 deletions pkg/tbtc/deduplicator.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ const (
// DKGResultHashCachePeriod is the time period the cache maintains
// the given DKG result hash.
DKGResultHashCachePeriod = 7 * 24 * time.Hour
// WalletClosedCachePeriod is the time period the cache maintains the ID of
// a closed wallet.
WalletClosedCachePeriod = 7 * 24 * time.Hour
)

// deduplicator decides whether the given event should be handled by the
Expand All @@ -31,15 +34,18 @@ const (
// Those events are supported:
// - DKG started
// - DKG result submitted
// - Wallet closed
type deduplicator struct {
dkgSeedCache *cache.TimeCache
dkgResultHashCache *cache.TimeCache
walletClosedCache *cache.TimeCache
}

func newDeduplicator() *deduplicator {
return &deduplicator{
dkgSeedCache: cache.NewTimeCache(DKGSeedCachePeriod),
dkgResultHashCache: cache.NewTimeCache(DKGResultHashCachePeriod),
walletClosedCache: cache.NewTimeCache(WalletClosedCachePeriod),
}
}

Expand Down Expand Up @@ -90,3 +96,23 @@ func (d *deduplicator) notifyDKGResultSubmitted(
// proceed with the execution.
return false
}

func (d *deduplicator) notifyWalletClosed(
WalletID [32]byte,
) bool {
d.walletClosedCache.Sweep()

// Use wallet ID converted to string as the cache key.
cacheKey := hex.EncodeToString(WalletID[:])

// If the key is not in the cache, that means the wallet closure was not
// handled yet and the client should proceed with the execution.
if !d.walletClosedCache.Has(cacheKey) {
d.walletClosedCache.Add(cacheKey)
return true
}

// Otherwise, the wallet closure is a duplicate and the client should not
// proceed with the execution.
return false
}
43 changes: 41 additions & 2 deletions pkg/tbtc/deduplicator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@ import (
"github.com/keep-network/keep-common/pkg/cache"
)

const testDKGSeedCachePeriod = 1 * time.Second
const testDKGResultHashCachePeriod = 1 * time.Second
const (
testDKGSeedCachePeriod = 1 * time.Second
testDKGResultHashCachePeriod = 1 * time.Second
testWalletClosedCachePeriod = 1 * time.Second
)

func TestNotifyDKGStarted(t *testing.T) {
deduplicator := deduplicator{
Expand Down Expand Up @@ -112,3 +115,39 @@ func TestNotifyDKGResultSubmitted(t *testing.T) {
t.Fatal("should be allowed to process")
}
}

func TestNotifyWalletClosed(t *testing.T) {
deduplicator := deduplicator{
walletClosedCache: cache.NewTimeCache(testWalletClosedCachePeriod),
}

wallet1 := [32]byte{1}
wallet2 := [32]byte{2}

// Add the first wallet ID.
canProcess := deduplicator.notifyWalletClosed(wallet1)
if !canProcess {
t.Fatal("should be allowed to process")
}

// Add the second wallet ID.
canProcess = deduplicator.notifyWalletClosed(wallet2)
if !canProcess {
t.Fatal("should be allowed to process")
}

// Add the first wallet ID before caching period elapses.
canProcess = deduplicator.notifyWalletClosed(wallet1)
if canProcess {
t.Fatal("should not be allowed to process")
}

// Wait until caching period elapses.
time.Sleep(testWalletClosedCachePeriod)

// Add the first wallet ID again.
canProcess = deduplicator.notifyWalletClosed(wallet1)
if !canProcess {
t.Fatal("should be allowed to process")
}
}
9 changes: 8 additions & 1 deletion pkg/tbtc/dkg_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,14 @@ func TestDkgExecutor_RegisterSigner(t *testing.T) {
for testName, test := range tests {
t.Run(testName, func(t *testing.T) {
persistenceHandle := &mockPersistenceHandle{}
walletRegistry := newWalletRegistry(persistenceHandle)
chain := Connect()
walletRegistry, err := newWalletRegistry(
persistenceHandle,
chain.CalculateWalletID,
)
if err != nil {
t.Fatal(err)
}

dkgExecutor := &dkgExecutor{
// setting only the fields really needed for this test
Expand Down
Loading

0 comments on commit efd3f20

Please sign in to comment.