node: pythnet testnet support (#1380)

This commit is contained in:
Evan Gray 2022-07-28 12:30:00 -05:00 committed by GitHub
parent d010f0d430
commit e0fd3e788f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 121 additions and 42 deletions

View File

@ -128,6 +128,10 @@ var (
solanaWsRPC *string
solanaRPC *string
pythnetContract *string
pythnetWsRPC *string
pythnetRPC *string
logLevel *string
unsafeDevMode *bool
@ -238,6 +242,10 @@ func init() {
solanaWsRPC = NodeCmd.Flags().String("solanaWS", "", "Solana Websocket URL (required")
solanaRPC = NodeCmd.Flags().String("solanaRPC", "", "Solana RPC URL (required")
pythnetContract = NodeCmd.Flags().String("pythnetContract", "", "Address of the PythNet program (required)")
pythnetWsRPC = NodeCmd.Flags().String("pythnetWS", "", "PythNet Websocket URL (required")
pythnetRPC = NodeCmd.Flags().String("pythnetRPC", "", "PythNet RPC URL (required")
logLevel = NodeCmd.Flags().String("logLevel", "info", "Logging level (debug, info, warn, error, dpanic, panic, fatal)")
unsafeDevMode = NodeCmd.Flags().Bool("unsafeDevMode", false, "Launch node in unsafe, deterministic devnet mode")
@ -358,6 +366,9 @@ func runNode(cmd *cobra.Command, args []string) {
if *solanaWsRPC != "" {
readiness.RegisterComponent(common.ReadinessSolanaSyncing)
}
if *pythnetWsRPC != "" {
readiness.RegisterComponent(common.ReadinessPythNetSyncing)
}
if *terraWS != "" {
readiness.RegisterComponent(common.ReadinessTerraSyncing)
}
@ -621,6 +632,16 @@ func runNode(cmd *cobra.Command, args []string) {
if *algorandAppID == 0 {
logger.Fatal("Please specify --algorandAppID")
}
if *pythnetContract == "" {
logger.Fatal("Please specify --pythnetContract")
}
if *pythnetWsRPC == "" {
logger.Fatal("Please specify --pythnetWsUrl")
}
if *pythnetRPC == "" {
logger.Fatal("Please specify --pythnetUrl")
}
}
}
@ -684,6 +705,13 @@ func runNode(cmd *cobra.Command, args []string) {
if err != nil {
logger.Fatal("invalid Solana contract address", zap.Error(err))
}
var pythnetAddress solana_types.PublicKey
if *testnetMode {
pythnetAddress, err = solana_types.PublicKeyFromBase58(*pythnetContract)
if err != nil {
logger.Fatal("invalid PythNet contract address", zap.Error(err))
}
}
// In devnet mode, we generate a deterministic guardian key and write it to disk.
if *unsafeDevMode {
@ -778,6 +806,7 @@ func runNode(cmd *cobra.Command, args []string) {
chainObsvReqC[vaa.ChainIDNeon] = make(chan *gossipv1.ObservationRequest)
chainObsvReqC[vaa.ChainIDEthereumRopsten] = make(chan *gossipv1.ObservationRequest)
chainObsvReqC[vaa.ChainIDInjective] = make(chan *gossipv1.ObservationRequest)
chainObsvReqC[vaa.ChainIDPythNet] = make(chan *gossipv1.ObservationRequest)
}
// Multiplex observation requests to the appropriate chain
@ -1007,12 +1036,24 @@ func runNode(cmd *cobra.Command, args []string) {
if *solanaWsRPC != "" {
if err := supervisor.Run(ctx, "solwatch-confirmed",
solana.NewSolanaWatcher(*solanaWsRPC, *solanaRPC, solAddress, lockC, nil, rpc.CommitmentConfirmed).Run); err != nil {
solana.NewSolanaWatcher(*solanaWsRPC, *solanaRPC, solAddress, lockC, nil, rpc.CommitmentConfirmed, common.ReadinessSolanaSyncing, vaa.ChainIDSolana).Run); err != nil {
return err
}
if err := supervisor.Run(ctx, "solwatch-finalized",
solana.NewSolanaWatcher(*solanaWsRPC, *solanaRPC, solAddress, lockC, chainObsvReqC[vaa.ChainIDSolana], rpc.CommitmentFinalized).Run); err != nil {
solana.NewSolanaWatcher(*solanaWsRPC, *solanaRPC, solAddress, lockC, chainObsvReqC[vaa.ChainIDSolana], rpc.CommitmentFinalized, common.ReadinessSolanaSyncing, vaa.ChainIDSolana).Run); err != nil {
return err
}
}
if *pythnetWsRPC != "" {
if err := supervisor.Run(ctx, "pythwatch-confirmed",
solana.NewSolanaWatcher(*pythnetWsRPC, *pythnetRPC, pythnetAddress, lockC, nil, rpc.CommitmentConfirmed, common.ReadinessPythNetSyncing, vaa.ChainIDPythNet).Run); err != nil {
return err
}
if err := supervisor.Run(ctx, "pythwatch-finalized",
solana.NewSolanaWatcher(*pythnetWsRPC, *pythnetRPC, pythnetAddress, lockC, chainObsvReqC[vaa.ChainIDPythNet], rpc.CommitmentFinalized, common.ReadinessPythNetSyncing, vaa.ChainIDPythNet).Run); err != nil {
return err
}
}

View File

@ -22,4 +22,5 @@ const (
ReadinessNeonSyncing readiness.Component = "neonSyncing"
ReadinessTerra2Syncing readiness.Component = "terra2Syncing"
ReadinessInjectiveSyncing readiness.Component = "injectiveSyncing"
ReadinessPythNetSyncing readiness.Component = "pythnetSyncing"
)

View File

@ -31,6 +31,12 @@ type SolanaWatcher struct {
messageEvent chan *common.MessagePublication
obsvReqC chan *gossipv1.ObservationRequest
rpcClient *rpc.Client
// Readiness component
readiness readiness.Component
// VAA ChainID of the network we're connecting to.
chainID vaa.ChainID
// Human readable name of network
networkName string
}
var (
@ -38,27 +44,27 @@ var (
prometheus.CounterOpts{
Name: "wormhole_solana_connection_errors_total",
Help: "Total number of Solana connection errors",
}, []string{"commitment", "reason"})
}, []string{"solana_network", "commitment", "reason"})
solanaAccountSkips = promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "wormhole_solana_account_updates_skipped_total",
Help: "Total number of account updates skipped due to invalid data",
}, []string{"reason"})
solanaMessagesConfirmed = promauto.NewCounter(
}, []string{"solana_network", "reason"})
solanaMessagesConfirmed = promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "wormhole_solana_observations_confirmed_total",
Help: "Total number of verified Solana observations found",
})
}, []string{"solana_network"})
currentSolanaHeight = promauto.NewGaugeVec(
prometheus.GaugeOpts{
Name: "wormhole_solana_current_height",
Help: "Current Solana slot height",
}, []string{"commitment"})
}, []string{"solana_network", "commitment"})
queryLatency = promauto.NewHistogramVec(
prometheus.HistogramOpts{
Name: "wormhole_solana_query_latency",
Help: "Latency histogram for Solana RPC calls",
}, []string{"operation", "commitment"})
}, []string{"solana_network", "operation", "commitment"})
)
const rpcTimeout = time.Second * 5
@ -105,7 +111,9 @@ func NewSolanaWatcher(
contractAddress solana.PublicKey,
messageEvents chan *common.MessagePublication,
obsvReqC chan *gossipv1.ObservationRequest,
commitment rpc.CommitmentType) *SolanaWatcher {
commitment rpc.CommitmentType,
readiness readiness.Component,
chainID vaa.ChainID) *SolanaWatcher {
return &SolanaWatcher{
contract: contractAddress,
wsUrl: wsUrl, rpcUrl: rpcUrl,
@ -113,13 +121,16 @@ func NewSolanaWatcher(
obsvReqC: obsvReqC,
commitment: commitment,
rpcClient: rpc.New(rpcUrl),
readiness: readiness,
chainID: chainID,
networkName: vaa.ChainID(chainID).String(),
}
}
func (s *SolanaWatcher) Run(ctx context.Context) error {
// Initialize gossip metrics (we want to broadcast the address even if we're not yet syncing)
contractAddr := base58.Encode(s.contract[:])
p2p.DefaultRegistry.SetNetworkStats(vaa.ChainIDSolana, &gossipv1.Heartbeat_Network{
p2p.DefaultRegistry.SetNetworkStats(s.chainID, &gossipv1.Heartbeat_Network{
ContractAddress: contractAddr,
})
@ -136,7 +147,7 @@ func (s *SolanaWatcher) Run(ctx context.Context) error {
case <-ctx.Done():
return
case m := <-s.obsvReqC:
if m.ChainId != uint32(vaa.ChainIDSolana) {
if m.ChainId != uint32(s.chainID) {
panic("unexpected chain id")
}
@ -152,19 +163,19 @@ func (s *SolanaWatcher) Run(ctx context.Context) error {
start := time.Now()
slot, err := s.rpcClient.GetSlot(rCtx, s.commitment)
cancel()
queryLatency.WithLabelValues("get_slot", string(s.commitment)).Observe(time.Since(start).Seconds())
queryLatency.WithLabelValues(s.networkName, "get_slot", string(s.commitment)).Observe(time.Since(start).Seconds())
if err != nil {
p2p.DefaultRegistry.AddErrorCount(vaa.ChainIDSolana, 1)
solanaConnectionErrors.WithLabelValues(string(s.commitment), "get_slot_error").Inc()
p2p.DefaultRegistry.AddErrorCount(s.chainID, 1)
solanaConnectionErrors.WithLabelValues(s.networkName, string(s.commitment), "get_slot_error").Inc()
errC <- err
return
}
if lastSlot == 0 {
lastSlot = slot - 1
}
currentSolanaHeight.WithLabelValues(string(s.commitment)).Set(float64(slot))
readiness.SetReady(common.ReadinessSolanaSyncing)
p2p.DefaultRegistry.SetNetworkStats(vaa.ChainIDSolana, &gossipv1.Heartbeat_Network{
currentSolanaHeight.WithLabelValues(s.networkName, string(s.commitment)).Set(float64(slot))
readiness.SetReady(s.readiness)
p2p.DefaultRegistry.SetNetworkStats(s.chainID, &gossipv1.Heartbeat_Network{
Height: int64(slot),
ContractAddress: contractAddr,
})
@ -240,7 +251,7 @@ func (s *SolanaWatcher) fetchBlock(ctx context.Context, logger *zap.Logger, slot
Commitment: s.commitment,
})
queryLatency.WithLabelValues("get_confirmed_block", string(s.commitment)).Observe(time.Since(start).Seconds())
queryLatency.WithLabelValues(s.networkName, "get_confirmed_block", string(s.commitment)).Observe(time.Since(start).Seconds())
if err != nil {
var rpcErr *jsonrpc.RPCError
if errors.As(err, &rpcErr) && (rpcErr.Code == -32007 /* SLOT_SKIPPED */ || rpcErr.Code == -32004 /* BLOCK_NOT_AVAILABLE */) {
@ -268,17 +279,17 @@ func (s *SolanaWatcher) fetchBlock(ctx context.Context, logger *zap.Logger, slot
} else {
logger.Error("failed to request block", zap.Error(err), zap.Uint64("slot", slot),
zap.String("commitment", string(s.commitment)))
p2p.DefaultRegistry.AddErrorCount(vaa.ChainIDSolana, 1)
solanaConnectionErrors.WithLabelValues(string(s.commitment), "get_confirmed_block_error").Inc()
p2p.DefaultRegistry.AddErrorCount(s.chainID, 1)
solanaConnectionErrors.WithLabelValues(s.networkName, string(s.commitment), "get_confirmed_block_error").Inc()
}
return false
}
if out == nil {
solanaConnectionErrors.WithLabelValues(string(s.commitment), "get_confirmed_block_error").Inc()
solanaConnectionErrors.WithLabelValues(s.networkName, string(s.commitment), "get_confirmed_block_error").Inc()
logger.Error("nil response when requesting block", zap.Error(err), zap.Uint64("slot", slot),
zap.String("commitment", string(s.commitment)))
p2p.DefaultRegistry.AddErrorCount(vaa.ChainIDSolana, 1)
p2p.DefaultRegistry.AddErrorCount(s.chainID, 1)
return false
}
@ -340,10 +351,10 @@ OUTER:
Commitment: s.commitment,
})
cancel()
queryLatency.WithLabelValues("get_confirmed_transaction", string(s.commitment)).Observe(time.Since(start).Seconds())
queryLatency.WithLabelValues(s.networkName, "get_confirmed_transaction", string(s.commitment)).Observe(time.Since(start).Seconds())
if err != nil {
p2p.DefaultRegistry.AddErrorCount(vaa.ChainIDSolana, 1)
solanaConnectionErrors.WithLabelValues(string(s.commitment), "get_confirmed_transaction_error").Inc()
p2p.DefaultRegistry.AddErrorCount(s.chainID, 1)
solanaConnectionErrors.WithLabelValues(s.networkName, string(s.commitment), "get_confirmed_transaction_error").Inc()
logger.Error("failed to request transaction",
zap.Error(err),
zap.Uint64("slot", slot),
@ -464,10 +475,10 @@ func (s *SolanaWatcher) fetchMessageAccount(ctx context.Context, logger *zap.Log
Encoding: solana.EncodingBase64,
Commitment: s.commitment,
})
queryLatency.WithLabelValues("get_account_info", string(s.commitment)).Observe(time.Since(start).Seconds())
queryLatency.WithLabelValues(s.networkName, "get_account_info", string(s.commitment)).Observe(time.Since(start).Seconds())
if err != nil {
p2p.DefaultRegistry.AddErrorCount(vaa.ChainIDSolana, 1)
solanaConnectionErrors.WithLabelValues(string(s.commitment), "get_account_info_error").Inc()
p2p.DefaultRegistry.AddErrorCount(s.chainID, 1)
solanaConnectionErrors.WithLabelValues(s.networkName, string(s.commitment), "get_account_info_error").Inc()
logger.Error("failed to request account",
zap.Error(err),
zap.Uint64("slot", slot),
@ -477,8 +488,8 @@ func (s *SolanaWatcher) fetchMessageAccount(ctx context.Context, logger *zap.Log
}
if !info.Value.Owner.Equals(s.contract) {
p2p.DefaultRegistry.AddErrorCount(vaa.ChainIDSolana, 1)
solanaConnectionErrors.WithLabelValues(string(s.commitment), "account_owner_mismatch").Inc()
p2p.DefaultRegistry.AddErrorCount(s.chainID, 1)
solanaConnectionErrors.WithLabelValues(s.networkName, string(s.commitment), "account_owner_mismatch").Inc()
logger.Error("account has invalid owner",
zap.Uint64("slot", slot),
zap.String("commitment", string(s.commitment)),
@ -489,8 +500,8 @@ func (s *SolanaWatcher) fetchMessageAccount(ctx context.Context, logger *zap.Log
data := info.Value.Data.GetBinary()
if string(data[:3]) != "msg" && string(data[:3]) != "msu" {
p2p.DefaultRegistry.AddErrorCount(vaa.ChainIDSolana, 1)
solanaConnectionErrors.WithLabelValues(string(s.commitment), "bad_account_data").Inc()
p2p.DefaultRegistry.AddErrorCount(s.chainID, 1)
solanaConnectionErrors.WithLabelValues(s.networkName, string(s.commitment), "bad_account_data").Inc()
logger.Error("account is not a message account",
zap.Uint64("slot", slot),
zap.String("commitment", string(s.commitment)),
@ -511,7 +522,7 @@ func (s *SolanaWatcher) fetchMessageAccount(ctx context.Context, logger *zap.Log
func (s *SolanaWatcher) processMessageAccount(logger *zap.Logger, data []byte, acc solana.PublicKey) {
proposal, err := ParseMessagePublicationAccount(data)
if err != nil {
solanaAccountSkips.WithLabelValues("parse_transfer_out").Inc()
solanaAccountSkips.WithLabelValues(s.networkName, "parse_transfer_out").Inc()
logger.Error(
"failed to parse transfer proposal",
zap.Stringer("account", acc),
@ -528,13 +539,13 @@ func (s *SolanaWatcher) processMessageAccount(logger *zap.Logger, data []byte, a
Timestamp: time.Unix(int64(proposal.SubmissionTime), 0),
Nonce: proposal.Nonce,
Sequence: proposal.Sequence,
EmitterChain: vaa.ChainIDSolana,
EmitterChain: s.chainID,
EmitterAddress: proposal.EmitterAddress,
Payload: proposal.Payload,
ConsistencyLevel: proposal.ConsistencyLevel,
}
solanaMessagesConfirmed.Inc()
solanaMessagesConfirmed.WithLabelValues(s.networkName).Inc()
logger.Info("message observed",
zap.Stringer("account", acc),

View File

@ -132,6 +132,8 @@ func (c ChainID) String() string {
return "terra2"
case ChainIDInjective:
return "injective"
case ChainIDPythNet:
return "pythnet"
default:
return fmt.Sprintf("unknown chain ID: %d", c)
}
@ -179,6 +181,8 @@ func ChainIDFromString(s string) (ChainID, error) {
return ChainIDTerra2, nil
case "injective":
return ChainIDInjective, nil
case "pythnet":
return ChainIDPythNet, nil
default:
return ChainIDUnset, fmt.Errorf("unknown chain ID: %s", s)
}
@ -222,6 +226,8 @@ const (
ChainIDTerra2 ChainID = 18
// ChainIDInjective is the ChainID of Injective
ChainIDInjective ChainID = 19
// ChainIDPythNet is the ChainID of PythNet
ChainIDPythNet ChainID = 26
// ChainIDEthereumRopsten is the ChainID of Ethereum Ropsten
ChainIDEthereumRopsten ChainID = 10001

View File

@ -34,6 +34,7 @@ enum ChainID {
CHAIN_ID_ARBITRUM = 23;
CHAIN_ID_OPTIMISM = 24;
CHAIN_ID_GNOSIS = 25;
CHAIN_ID_PYTHNET = 26;
// Special case - Eth has two testnets. CHAIN_ID_ETHEREUM is Goerli,
// but we also want to connect to Ropsten, so we add a separate chain.
CHAIN_ID_ETHEREUM_ROPSTEN = 10001;

View File

@ -2,16 +2,17 @@
## 0.5.2
### Changed
### Added
Added chain ids for Arbitrum, Optimism, and Gnosis
Support for PythNet
Chain ids for Arbitrum, Optimism, and Gnosis
## 0.5.1
### Changed
### Added
Chain ids for Injective, Osmosis, Sui, and Aptos
Added chain ids for Injective, Osmosis, Sui, and Aptos
## 0.5.0
### Changed

View File

@ -24,6 +24,7 @@ import {
coalesceChainId,
isEVMChain,
isTerraChain,
CHAIN_ID_PYTHNET,
} from "./consts";
/**
@ -72,7 +73,7 @@ export const tryUint8ArrayToNative = (
const chainId = coalesceChainId(chain);
if (isEVMChain(chainId)) {
return hexZeroPad(hexValue(a), 20);
} else if (chainId === CHAIN_ID_SOLANA) {
} else if (chainId === CHAIN_ID_SOLANA || chainId === CHAIN_ID_PYTHNET) {
return new PublicKey(a).toString();
} else if (isTerraChain(chainId)) {
const h = uint8ArrayToHex(a);
@ -188,7 +189,7 @@ export const tryNativeToHexString = (
const chainId = coalesceChainId(chain);
if (isEVMChain(chainId)) {
return uint8ArrayToHex(zeroPad(arrayify(address), 32));
} else if (chainId === CHAIN_ID_SOLANA) {
} else if (chainId === CHAIN_ID_SOLANA || chainId === CHAIN_ID_PYTHNET) {
return uint8ArrayToHex(zeroPad(new PublicKey(address).toBytes(), 32));
} else if (chainId === CHAIN_ID_TERRA) {
if (isNativeDenom(address)) {

View File

@ -25,6 +25,7 @@ export const CHAINS = {
arbitrum: 23,
optimism: 24,
gnosis: 25,
pythnet: 26,
ropsten: 10001,
} as const;
@ -198,6 +199,11 @@ const MAINNET = {
token_bridge: undefined,
nft_bridge: undefined,
},
pythnet: {
core: undefined,
token_bridge: undefined,
nft_bridge: undefined,
},
ropsten: {
core: undefined,
token_bridge: undefined,
@ -337,6 +343,11 @@ const TESTNET = {
token_bridge: undefined,
nft_bridge: undefined,
},
pythnet: {
core: undefined,
token_bridge: undefined,
nft_bridge: undefined,
},
ropsten: {
core: "0x210c5F5e2AF958B4defFe715Dc621b7a3BA888c5",
token_bridge: "0xF174F9A837536C449321df1Ca093Bb96948D5386",
@ -476,6 +487,11 @@ const DEVNET = {
token_bridge: undefined,
nft_bridge: undefined,
},
pythnet: {
core: undefined,
token_bridge: undefined,
nft_bridge: undefined,
},
ropsten: {
core: undefined,
token_bridge: undefined,
@ -547,6 +563,7 @@ export const CHAIN_ID_APTOS = CHAINS["aptos"];
export const CHAIN_ID_ARBITRUM = CHAINS["arbitrum"];
export const CHAIN_ID_OPTIMISM = CHAINS["optimism"];
export const CHAIN_ID_GNOSIS = CHAINS["gnosis"];
export const CHAIN_ID_PYTHNET = CHAINS["pythnet"];
export const CHAIN_ID_ETHEREUM_ROPSTEN = CHAINS["ropsten"];
// This inverts the [[CHAINS]] object so that we can look up a chain by id