Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[tmpnet] Add support for checking rpcchainvm version compatibility #3276

Merged
merged 7 commits into from
Aug 15, 2024
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
1 change: 1 addition & 0 deletions tests/fixture/subnet/xsvm.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ func NewXSVMOrPanic(name string, key *secp256k1.PrivateKey, nodes ...*tmpnet.Nod
VMID: constants.XSVMID,
Genesis: genesisBytes,
PreFundedKey: key,
VersionArgs: []string{"version-json"},
},
},
ValidatorIDs: tmpnet.NodesToIDs(nodes...),
Expand Down
11 changes: 8 additions & 3 deletions tests/fixture/tmpnet/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,14 @@ network := &tmpnet.Network{ // Configure non-default values fo
Name: "xsvm-a", // User-defined name used to reference subnet in code and on disk
Chains: []*tmpnet.Chain{
{
VMName: "xsvm", // Name of the VM the chain will run, will be used to derive the name of the VM binary
Genesis: <genesis bytes>, // Genesis bytes used to initialize the custom chain
PreFundedKey: <key>, // (Optional) A private key that is funded in the genesis bytes
VMName: "xsvm", // Name of the VM the chain will run, will be used to derive the name of the VM binary
Genesis: <genesis bytes>, // Genesis bytes used to initialize the custom chain
PreFundedKey: <key>, // (Optional) A private key that is funded in the genesis bytes
VersionArgs: "version-json", // (Optional) Arguments that prompt the VM binary to output version details in json format.
// If one or more arguments are provided, the resulting json output should include a field
// named `rpcchainvm` of type uint64 containing the rpc version supported by the VM binary.
// The version will be checked against the version reported by the configured avalanchego
// binary before network and node start.
},
},
ValidatorIDs: <node ids>, // The IDs of nodes that validate the subnet
Expand Down
66 changes: 62 additions & 4 deletions tests/fixture/tmpnet/network.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ package tmpnet
import (
"context"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"slices"
"strconv"
Expand Down Expand Up @@ -141,7 +143,7 @@ func BootstrapNewNetwork(
if len(network.Nodes) == 0 {
return errInsufficientNodes
}
if err := checkVMBinariesExist(network.Subnets, pluginDir); err != nil {
if err := checkVMBinaries(w, network.Subnets, avalancheGoExecPath, pluginDir); err != nil {
return err
}
if err := network.EnsureDefaultConfig(w, avalancheGoExecPath, pluginDir); err != nil {
Expand Down Expand Up @@ -465,11 +467,13 @@ func (n *Network) StartNode(ctx context.Context, w io.Writer, node *Node) error
if err != nil {
return err
}
if err := checkVMBinariesExist(n.Subnets, pluginDir); err != nil {

if err := n.EnsureNodeConfig(node); err != nil {
return err
}

if err := n.EnsureNodeConfig(node); err != nil {
// Check the VM binaries after EnsureNodeConfig to ensure node.RuntimeConfig is non-nil
if err := checkVMBinaries(w, n.Subnets, node.RuntimeConfig.AvalancheGoPath, pluginDir); err != nil {
return err
}

Expand Down Expand Up @@ -939,15 +943,69 @@ func GetReusableNetworkPathForOwner(owner string) (string, error) {
return filepath.Join(networkPath, "latest_"+owner), nil
}

func checkVMBinariesExist(subnets []*Subnet, pluginDir string) error {
const invalidRPCVersion = 0

// checkVMBinaries checks that VM binaries for the given subnets exist and optionally checks that VM
// binaries have the same rpcchainvm version as the indicated avalanchego binary.
func checkVMBinaries(w io.Writer, subnets []*Subnet, avalanchegoPath string, pluginDir string) error {
if len(subnets) == 0 {
return nil
}

expectedRPCVersion, err := getRPCVersion(avalanchegoPath, "--version-json")
if err != nil {
// Only warn if the rpc version is not available to ensure backwards compatibility.
if _, err := fmt.Fprintf(w, "Warning: Unable to check rpcchainvm version for avalanchego: %v\n", err); err != nil {
return err
}
}

errs := []error{}
for _, subnet := range subnets {
for _, chain := range subnet.Chains {
pluginPath := filepath.Join(pluginDir, chain.VMID.String())

// Check that the path exists
if _, err := os.Stat(pluginPath); err != nil {
errs = append(errs, fmt.Errorf("failed to check VM binary for subnet %q: %w", subnet.Name, err))
}

if len(chain.VersionArgs) == 0 || expectedRPCVersion == invalidRPCVersion {
// Not possible to check the rpcchainvm version
continue
}

// Check that the VM's rpcchainvm version matches avalanchego's version
rpcVersion, err := getRPCVersion(pluginPath, chain.VersionArgs...)
if err != nil {
if _, err := fmt.Fprintf(w, "Warning: Unable to check rpcchainvm version for VM Binary for subnet %q: %v\n", subnet.Name, err); err != nil {
return err
}
} else if expectedRPCVersion != rpcVersion {
errs = append(errs, fmt.Errorf("unexpected rpcchainvm version for VM binary of subnet %q: %q reports %d, but %q reports %d", subnet.Name, avalanchegoPath, expectedRPCVersion, pluginPath, rpcVersion))
}
}
}

return errors.Join(errs...)
}

type RPCChainVMVersion struct {
RPCChainVM uint64 `json:"rpcchainvm"`
}

// getRPCVersion attempts to invoke the given command with the specified version arguments and
// retrieve an rpcchainvm version from its output.
func getRPCVersion(command string, versionArgs ...string) (uint64, error) {
cmd := exec.Command(command, versionArgs...)
output, err := cmd.CombinedOutput()
if err != nil {
return 0, fmt.Errorf("command %q failed with output: %s", command, output)
}
version := &RPCChainVMVersion{}
if err := json.Unmarshal(output, version); err != nil {
return 0, fmt.Errorf("failed to unmarshal output from command %q: %w, output: %s", command, err, output)
}

return version.RPCChainVM, nil
}
6 changes: 6 additions & 0 deletions tests/fixture/tmpnet/subnet.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ type Chain struct {
VMID ids.ID
Config string
Genesis []byte
// VersionArgs are the argument(s) to pass to the VM binary to receive
// version details in json format (e.g. `--version-json`). This
// supports checking that the rpcchainvm version of the VM binary
// matches the version used by the configured avalanchego binary. If
// empty, the version check will be skipped.
VersionArgs []string

// Set at runtime
ChainID ids.ID
Expand Down
1 change: 1 addition & 0 deletions vms/example/xsvm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ Available Commands:
help Help about any command
issue Issues transactions
version Prints out the version
versionjson Prints out the version in json format

Flags:
-h, --help help for xsvm
Expand Down
46 changes: 46 additions & 0 deletions vms/example/xsvm/cmd/versionjson/cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

package versionjson

import (
"encoding/json"
"fmt"

"github.com/spf13/cobra"

"github.com/ava-labs/avalanchego/ids"
"github.com/ava-labs/avalanchego/utils/constants"
"github.com/ava-labs/avalanchego/version"
"github.com/ava-labs/avalanchego/vms/example/xsvm"
)

type vmVersions struct {
Name string `json:"name"`
VMID ids.ID `json:"vmid"`
Version *version.Semantic `json:"version"`
RPCChainVM uint64 `json:"rpcchainvm"`
}

func Command() *cobra.Command {
return &cobra.Command{
Use: "version-json",
Short: "Prints out the version in json format",
RunE: versionFunc,
}
}

func versionFunc(*cobra.Command, []string) error {
versions := vmVersions{
Name: constants.XSVMName,
VMID: constants.XSVMID,
Version: xsvm.Version,
RPCChainVM: uint64(version.RPCChainVMProtocol),
}
jsonBytes, err := json.MarshalIndent(versions, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal versions: %w", err)
}
fmt.Println(string(jsonBytes))
return nil
}
2 changes: 2 additions & 0 deletions vms/example/xsvm/cmd/xsvm/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/ava-labs/avalanchego/vms/example/xsvm/cmd/issue"
"github.com/ava-labs/avalanchego/vms/example/xsvm/cmd/run"
"github.com/ava-labs/avalanchego/vms/example/xsvm/cmd/version"
"github.com/ava-labs/avalanchego/vms/example/xsvm/cmd/versionjson"
)

func init() {
Expand All @@ -28,6 +29,7 @@ func main() {
chain.Command(),
issue.Command(),
version.Command(),
versionjson.Command(),
)
ctx := context.Background()
if err := cmd.ExecuteContext(ctx); err != nil {
Expand Down
Loading