Skip to content

Commit

Permalink
feat(zetaclient): Revamp TSS package (#3170)
Browse files Browse the repository at this point in the history
* Streamline tss config

* Revamp TSS keygen ceremony code

* Refactor tss key sign test; streamline sig verification

* Remove optional pub key (tss)

* Implement PubKey entity

* Implement new TSS service layer. Add unit tests

* Add prometheus metrics

* Restructure files

* Implement TSS Setup Flow

* Adapt some test cases; fix typo

* Merge pubkey.go with crypto.go

* Add tss key pass alongside with hotkey pass

* Simplify start.go; drop old tss from zetaclient

* Fix cyclic imports

* Revamp TSS mock. Fix unit tests across zetaclient

* Remove tss historical list from outbound evm cctx

* TSS healthcheck

* Fix flaky case when GetTSS() returns an empty string

* Update changelog

* Fix typos

* Forward docker pprof in e2e

* Forward pprof in e2e docker

* Simplify VerifySignature

* Fix typo

* Fix batch signature verification

* Add readme

* Fix typo

* Fix typos in healthcheck

* Address PR comments [1]

* Address PR comments [2]

* Address PR comments [3] (improve errors clarity)

* Address PR comments [4]

* Remove redundant methods from app context

* Address PR comments [5]

* Address PR comments [6] (test coverage)

* Fix TSS migration test

* Fix btc issue
  • Loading branch information
swift1337 committed Dec 2, 2024
1 parent e682c18 commit c0b954e
Show file tree
Hide file tree
Showing 66 changed files with 2,328 additions and 1,020 deletions.
3 changes: 2 additions & 1 deletion cmd/zetaclientd/initconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

"github.com/zeta-chain/node/testutil/sample"
"github.com/zeta-chain/node/zetaclient/config"
zetatss "github.com/zeta-chain/node/zetaclient/tss"
)

// initializeConfigOptions is a set of CLI options for `init` command.
Expand Down Expand Up @@ -73,7 +74,7 @@ func InitializeConfig(_ *cobra.Command, _ []string) error {
// Validate Peer
// e.g. /ip4/172.0.2.1/tcp/6668/p2p/16Uiu2HAmACG5DtqmQsHtXg4G2sLS65ttv84e7MrL4kapkjfmhxAp
if opts.peer != "" {
if err := validatePeer(opts.peer); err != nil {
if _, err := zetatss.MultiAddressFromString(opts.peer); err != nil {
return errors.Wrap(err, "invalid peer address")
}
}
Expand Down
32 changes: 15 additions & 17 deletions cmd/zetaclientd/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ package main
import (
"context"
"fmt"
"net"
"strings"
"os"
"strconv"
"time"

sdk "github.com/cosmos/cosmos-sdk/types"
Expand All @@ -16,6 +16,7 @@ import (
"github.com/zeta-chain/node/zetaclient/authz"
"github.com/zeta-chain/node/zetaclient/chains/interfaces"
"github.com/zeta-chain/node/zetaclient/config"
zctx "github.com/zeta-chain/node/zetaclient/context"
"github.com/zeta-chain/node/zetaclient/keys"
"github.com/zeta-chain/node/zetaclient/zetacore"
)
Expand Down Expand Up @@ -101,23 +102,20 @@ func waitForZetacoreToCreateBlocks(ctx context.Context, zc interfaces.ZetacoreCl
}
}

func validatePeer(seedPeer string) error {
parsedPeer := strings.Split(seedPeer, "/")

if len(parsedPeer) < 7 {
return errors.New("seed peer missing IP or ID or both, seed: " + seedPeer)
}

seedIP := parsedPeer[2]
seedID := parsedPeer[6]
func isEnvFlagEnabled(flag string) bool {
v, _ := strconv.ParseBool(os.Getenv(flag))
return v
}

if net.ParseIP(seedIP) == nil {
return errors.New("invalid seed IP address format, seed: " + seedPeer)
}
func btcChainIDsFromContext(app *zctx.AppContext) []int64 {
var (
btcChains = app.FilterChains(zctx.Chain.IsBitcoin)
btcChainIDs = make([]int64, len(btcChains))
)

if len(seedID) == 0 {
return errors.New("seed id is empty, seed: " + seedPeer)
for i, chain := range btcChains {
btcChainIDs[i] = chain.ID()
}

return nil
return btcChainIDs
}
3 changes: 3 additions & 0 deletions contrib/localnet/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,10 @@ services:
- ETHDEV_ENDPOINT=http://eth:8545
- HOTKEY_BACKEND=file
- HOTKEY_PASSWORD=password # test purposes only
- PPROF_ADDR=0.0.0.0:6061
restart: always
ports:
- "6061:6061" # pprof
volumes:
- ssh:/root/.ssh
- preparams:/root/preparams
Expand Down
3 changes: 2 additions & 1 deletion contrib/localnet/orchestrator/start-zetae2e.sh
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,8 @@ if [ "$LOCALNET_MODE" == "tss-migrate" ]; then
echo "waiting 10 seconds for node to restart"
sleep 10

zetae2e local --skip-setup --config "$deployed_config_path" --skip-bitcoin-setup --light --skip-header-proof
zetae2e local --skip-setup --config "$deployed_config_path" \
--skip-bitcoin-setup --light --skip-header-proof --skip-precompiles
ZETAE2E_EXIT_CODE=$?
if [ $ZETAE2E_EXIT_CODE -eq 0 ]; then
echo "E2E passed after migration"
Expand Down
3 changes: 2 additions & 1 deletion e2e/e2etests/test_migrate_tss.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,11 @@ import (
)

func TestMigrateTSS(r *runner.E2ERunner, _ []string) {
r.SetupBtcAddress(false)
stop := r.MineBlocksIfLocalBitcoin()
defer stop()

// Pause inbound procoessing for tss migration
// Pause inbound processing for tss migration
r.Logger.Info("Pause inbound processing")
msg := observertypes.NewMsgDisableCCTX(
r.ZetaTxServer.MustGetAccountAddressFromName(utils.EmergencyPolicyName),
Expand Down
2 changes: 2 additions & 0 deletions e2e/runner/bitcoin.go
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,8 @@ func (r *E2ERunner) QueryOutboundReceiverAndAmount(txid string) (string, int64)
// and returns a channel that can be used to stop the mining
// If the chain is not local, the function does nothing
func (r *E2ERunner) MineBlocksIfLocalBitcoin() func() {
require.NotNil(r, r.BTCDeployerAddress, "E2ERunner.BTCDeployerAddress is nil")

stopChan := make(chan struct{})
go func() {
for {
Expand Down
7 changes: 4 additions & 3 deletions pkg/cosmos/cosmos.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import (
"github.com/cosmos/cosmos-sdk/types/bech32/legacybech32" // nolint
)

const Bech32PubKeyTypeAccPub = legacybech32.AccPK

var (
GetPubKeyFromBech32 = legacybech32.UnmarshalPubKey
Bech32ifyPubKey = legacybech32.MarshalPubKey
Bech32PubKeyTypeAccPub = legacybech32.AccPK
GetPubKeyFromBech32 = legacybech32.UnmarshalPubKey
Bech32ifyPubKey = legacybech32.MarshalPubKey
)
4 changes: 2 additions & 2 deletions zetaclient/chains/base/observer.go
Original file line number Diff line number Diff line change
Expand Up @@ -228,13 +228,13 @@ func (ob *Observer) WithTSS(tss interfaces.TSSSigner) *Observer {
func (ob *Observer) TSSAddressString() string {
switch ob.chain.Consensus {
case chains.Consensus_bitcoin:
address, err := ob.tss.BTCAddress(ob.Chain().ChainId)
address, err := ob.tss.PubKey().AddressBTC(ob.Chain().ChainId)
if err != nil {
return ""
}
return address.EncodeAddress()
default:
return ob.tss.EVMAddress().String()
return ob.tss.PubKey().AddressEVM().String()
}
}

Expand Down
39 changes: 23 additions & 16 deletions zetaclient/chains/base/observer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,14 @@ import (
"context"
"fmt"
"os"
"strings"
"testing"
"time"

sdk "github.com/cosmos/cosmos-sdk/types"
lru "github.com/hashicorp/golang-lru"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/stretchr/testify/require"
"github.com/zeta-chain/node/cmd"
"github.com/zeta-chain/node/pkg/chains"
"github.com/zeta-chain/node/pkg/coin"
"github.com/zeta-chain/node/testutil/sample"
Expand All @@ -23,7 +22,6 @@ import (
zctx "github.com/zeta-chain/node/zetaclient/context"
"github.com/zeta-chain/node/zetaclient/db"
"github.com/zeta-chain/node/zetaclient/metrics"
"github.com/zeta-chain/node/zetaclient/testutils"
"github.com/zeta-chain/node/zetaclient/testutils/mocks"
)

Expand All @@ -41,7 +39,7 @@ func createObserver(t *testing.T, chain chains.Chain, alertLatency int64) *base.
chainParams := *sample.ChainParams(chain.ChainId)
chainParams.ConfirmationCount = defaultConfirmationCount
zetacoreClient := mocks.NewZetacoreClient(t)
tss := mocks.NewTSSMainnet()
tss := mocks.NewTSS(t)

database := createDatabase(t)

Expand Down Expand Up @@ -70,7 +68,7 @@ func TestNewObserver(t *testing.T) {
chainParams := *sample.ChainParams(chain.ChainId)
appContext := zctx.New(config.New(false), nil, zerolog.Nop())
zetacoreClient := mocks.NewZetacoreClient(t)
tss := mocks.NewTSSMainnet()
tss := mocks.NewTSS(t)
blockCacheSize := base.DefaultBlockCacheSize
headersCacheSize := base.DefaultHeaderCacheSize

Expand Down Expand Up @@ -188,7 +186,7 @@ func TestObserverGetterAndSetter(t *testing.T) {
ob := createObserver(t, chain, defaultAlertLatency)

// update tss
newTSS := mocks.NewTSSAthens3()
newTSS := mocks.NewTSS(t)
ob = ob.WithTSS(newTSS)
require.Equal(t, newTSS, ob.TSS())
})
Expand Down Expand Up @@ -266,9 +264,6 @@ func TestObserverGetterAndSetter(t *testing.T) {
}

func TestTSSAddressString(t *testing.T) {
testConfig := sdk.GetConfig()
testConfig.SetBech32PrefixForAccount(cmd.Bech32PrefixAccAddr, cmd.Bech32PrefixAccPub)

tests := []struct {
name string
chain chains.Chain
Expand All @@ -278,17 +273,17 @@ func TestTSSAddressString(t *testing.T) {
{
name: "should return TSS BTC address for Bitcoin chain",
chain: chains.BitcoinMainnet,
addrExpected: testutils.TSSAddressBTCMainnet,
addrExpected: "btc",
},
{
name: "should return TSS EVM address for EVM chain",
chain: chains.Ethereum,
addrExpected: testutils.TSSAddressEVMMainnet,
addrExpected: "eth",
},
{
name: "should return TSS EVM address for other non-BTC chain",
chain: chains.SolanaDevnet,
addrExpected: testutils.TSSAddressEVMMainnet,
addrExpected: "eth",
},
{
name: "should return empty address for unknown BTC chain",
Expand All @@ -307,14 +302,26 @@ func TestTSSAddressString(t *testing.T) {
// force error if needed
if tt.forceError {
// pause TSS to cause error
tss := mocks.NewTSSMainnet()
tss := mocks.NewTSS(t)
tss.Pause()
ob = ob.WithTSS(tss)
c := chains.BitcoinRegtest
c.ChainId = 123123123
ob.WithChain(c)
}

// get TSS address
addr := ob.TSSAddressString()
require.Equal(t, tt.addrExpected, addr)
switch tt.addrExpected {
case "":
require.Equal(t, "", addr)
case "btc":
require.True(t, strings.HasPrefix(addr, "bc"))
case "eth":
require.True(t, strings.HasPrefix(addr, "0x"))
default:
t.Fail()
}
})
}
}
Expand Down Expand Up @@ -374,13 +381,13 @@ func TestOutboundID(t *testing.T) {
{
name: "should get correct outbound id for Ethereum chain",
chain: chains.Ethereum,
tss: mocks.NewTSSMainnet(),
tss: mocks.NewTSS(t),
nonce: 100,
},
{
name: "should get correct outbound id for Bitcoin chain",
chain: chains.BitcoinMainnet,
tss: mocks.NewTSSMainnet(),
tss: mocks.NewTSS(t),
nonce: 200,
},
}
Expand Down
6 changes: 3 additions & 3 deletions zetaclient/chains/base/signer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ import (
)

// createSigner creates a new signer for testing
func createSigner(_ *testing.T) *base.Signer {
func createSigner(t *testing.T) *base.Signer {
// constructor parameters
chain := chains.Ethereum
tss := mocks.NewTSSMainnet()
tss := mocks.NewTSS(t)
logger := base.DefaultLogger()

// create signer
Expand All @@ -40,7 +40,7 @@ func TestSignerGetterAndSetter(t *testing.T) {
signer := createSigner(t)

// update tss
newTSS := mocks.NewTSSAthens3()
newTSS := mocks.NewTSS(t)
signer = signer.WithTSS(newTSS)
require.Equal(t, newTSS, signer.TSS())
})
Expand Down
2 changes: 1 addition & 1 deletion zetaclient/chains/bitcoin/observer/observer.go
Original file line number Diff line number Diff line change
Expand Up @@ -353,7 +353,7 @@ func (ob *Observer) FetchUTXOs(ctx context.Context) error {
maxConfirmations := int(bh)

// List all unspent UTXOs (160ms)
tssAddr, err := ob.TSS().BTCAddress(ob.Chain().ChainId)
tssAddr, err := ob.TSS().PubKey().AddressBTC(ob.Chain().ChainId)
if err != nil {
return fmt.Errorf("error getting bitcoin tss address")
}
Expand Down
6 changes: 3 additions & 3 deletions zetaclient/chains/bitcoin/observer/observer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,15 +126,15 @@ func Test_NewObserver(t *testing.T) {
btcClient: btcClient,
chainParams: params,
coreClient: nil,
tss: mocks.NewTSSMainnet(),
tss: mocks.NewTSS(t),
},
{
name: "should fail if net params is not found",
chain: chains.Chain{ChainId: 111}, // invalid chain id
btcClient: btcClient,
chainParams: params,
coreClient: nil,
tss: mocks.NewTSSMainnet(),
tss: mocks.NewTSS(t),
errorMessage: "unable to get BTC net params for chain",
},
{
Expand All @@ -143,7 +143,7 @@ func Test_NewObserver(t *testing.T) {
btcClient: btcClient,
chainParams: params,
coreClient: nil,
tss: mocks.NewTSSMainnet(),
tss: mocks.NewTSS(t),
before: func() {
envVar := base.EnvVarLatestBlockByChain(chain)
os.Setenv(envVar, "invalid")
Expand Down
2 changes: 1 addition & 1 deletion zetaclient/chains/bitcoin/observer/outbound.go
Original file line number Diff line number Diff line change
Expand Up @@ -560,7 +560,7 @@ func (ob *Observer) checkTSSVin(ctx context.Context, vins []btcjson.Vin, nonce u
if nonce > 0 && len(vins) <= 1 {
return fmt.Errorf("checkTSSVin: len(vins) <= 1")
}
pubKeyTss := hex.EncodeToString(ob.TSS().PubKeyCompressedBytes())
pubKeyTss := hex.EncodeToString(ob.TSS().PubKey().Bytes(true))
for i, vin := range vins {
// The length of the Witness should be always 2 for SegWit inputs.
if len(vin.Witness) != 2 {
Expand Down
16 changes: 9 additions & 7 deletions zetaclient/chains/bitcoin/observer/outbound_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/btcsuite/btcd/btcjson"
"github.com/ethereum/go-ethereum/crypto"
"github.com/rs/zerolog"
"github.com/stretchr/testify/require"
"github.com/zeta-chain/node/zetaclient/db"

Expand All @@ -25,7 +26,7 @@ func MockBTCObserverMainnet(t *testing.T) *Observer {
// setup mock arguments
chain := chains.BitcoinMainnet
params := mocks.MockChainParams(chain.ChainId, 10)
tss := mocks.NewTSSMainnet()
tss := mocks.NewTSS(t).FakePubKey(testutils.TSSPubKeyMainnet)

// create mock rpc client
btcClient := mocks.NewBTCRPCClient(t)
Expand All @@ -34,8 +35,11 @@ func MockBTCObserverMainnet(t *testing.T) *Observer {
database, err := db.NewFromSqliteInMemory(true)
require.NoError(t, err)

logger := zerolog.New(zerolog.NewTestWriter(t))
baseLogger := base.Logger{Std: logger, Compliance: logger}

// create Bitcoin observer
ob, err := NewObserver(chain, btcClient, params, nil, tss, 60, database, base.Logger{}, nil)
ob, err := NewObserver(chain, btcClient, params, nil, tss, 60, database, baseLogger, nil)
require.NoError(t, err)

return ob
Expand All @@ -46,9 +50,7 @@ func createObserverWithPrivateKey(t *testing.T) *Observer {
skHex := "7b8507ba117e069f4a3f456f505276084f8c92aee86ac78ae37b4d1801d35fa8"
privateKey, err := crypto.HexToECDSA(skHex)
require.NoError(t, err)
tss := &mocks.TSS{
PrivKey: privateKey,
}
tss := mocks.NewTSSFromPrivateKey(t, privateKey)

// create Bitcoin observer with mock tss
ob := MockBTCObserverMainnet(t)
Expand All @@ -61,7 +63,7 @@ func createObserverWithPrivateKey(t *testing.T) *Observer {
func createObserverWithUTXOs(t *testing.T) *Observer {
// Create Bitcoin observer
ob := createObserverWithPrivateKey(t)
tssAddress, err := ob.TSS().BTCAddress(chains.BitcoinTestnet.ChainId)
tssAddress, err := ob.TSS().PubKey().AddressBTC(chains.BitcoinTestnet.ChainId)
require.NoError(t, err)

// Create 10 dummy UTXOs (22.44 BTC in total)
Expand All @@ -79,7 +81,7 @@ func mineTxNSetNonceMark(t *testing.T, ob *Observer, nonce uint64, txid string,
ob.includedTxResults[outboundID] = &btcjson.GetTransactionResult{TxID: txid}

// Set nonce mark
tssAddress, err := ob.TSS().BTCAddress(chains.BitcoinTestnet.ChainId)
tssAddress, err := ob.TSS().PubKey().AddressBTC(chains.BitcoinMainnet.ChainId)
require.NoError(t, err)
nonceMark := btcjson.ListUnspentResult{
TxID: txid,
Expand Down
Loading

0 comments on commit c0b954e

Please sign in to comment.