diff --git a/build/cargo/cargo.go b/build/cargo/cargo.go new file mode 100644 index 00000000..df488d6c --- /dev/null +++ b/build/cargo/cargo.go @@ -0,0 +1,111 @@ +// Package cargo contains helper functions for building Rust applications using cargo. +package cargo + +import ( + "encoding/json" + "fmt" + "os/exec" +) + +// Metadata is the cargo package metadata. +type Metadata struct { + Name string + Version string +} + +// GetMetadata queries `cargo` for metadata of the package in the current working directory. +func GetMetadata() (*Metadata, error) { + cmd := exec.Command("cargo", "metadata", "--no-deps") + stdout, err := cmd.StdoutPipe() + if err != nil { + return nil, fmt.Errorf("failed to initialize metadata process: %w", err) + } + if err = cmd.Start(); err != nil { + return nil, fmt.Errorf("failed to start metadata process: %w", err) + } + + dec := json.NewDecoder(stdout) + var rawMeta struct { + Packages []struct { + Name string `json:"name"` + Version string `json:"version"` + } `json:"packages"` + } + if err = dec.Decode(&rawMeta); err != nil { + return nil, fmt.Errorf("malformed cargo metadata: %w", err) + } + if err = cmd.Wait(); err != nil { + return nil, fmt.Errorf("metadata process failed: %w", err) + } + if len(rawMeta.Packages) == 0 { + return nil, fmt.Errorf("no cargo packages found") + } + + return &Metadata{ + Name: rawMeta.Packages[0].Name, + Version: rawMeta.Packages[0].Version, + }, nil +} + +// Build builds a Rust program using `cargo` in the current working directory. +func Build(release bool, target string) (string, error) { + args := []string{"build"} + if release { + args = append(args, "--release") + } + if target != "" { + args = append(args, "--target", target) + } + // Ensure the build process outputs JSON. + args = append(args, "--message-format", "json") + + cmd := exec.Command("cargo", args...) + stdout, err := cmd.StdoutPipe() + if err != nil { + return "", fmt.Errorf("failed to initialize build process: %w", err) + } + if err = cmd.Start(); err != nil { + return "", fmt.Errorf("failed to start build process: %w", err) + } + + var executable string + dec := json.NewDecoder(stdout) + for { + var output struct { + Reason string `json:"reason"` + PackageID string `json:"package_id,omitempty"` + Target struct { + Kind []string `json:"kind"` + } `json:"target,omitempty"` + Executable string `json:"executable,omitempty"` + Message struct { + Rendered string `json:"rendered"` + } `json:"message,omitempty"` + } + if err = dec.Decode(&output); err != nil { + break + } + + switch output.Reason { + case "compiler-message": + fmt.Println(output.Message.Rendered) + case "compiler-artifact": + fmt.Printf("[built] %s\n", output.PackageID) + if len(output.Target.Kind) != 1 || output.Target.Kind[0] != "bin" { + continue + } + + // Extract the last built executable. + executable = output.Executable + default: + } + } + if err = cmd.Wait(); err != nil { + return "", fmt.Errorf("build process failed: %w", err) + } + + if executable == "" { + return "", fmt.Errorf("no executable generated") + } + return executable, nil +} diff --git a/build/sgxs/sgxs.go b/build/sgxs/sgxs.go new file mode 100644 index 00000000..fbfeb293 --- /dev/null +++ b/build/sgxs/sgxs.go @@ -0,0 +1,23 @@ +// Package sgxs contains helper functions for dealing with ELF and SGXS binaries. +package sgxs + +import ( + "os/exec" + "strconv" +) + +// Elf2Sgxs converts an ELF binary built for the SGX ABI into an SGXS binary. +// +// It requires the `ftxsgx-elf2sgxs` utility to be installed. +func Elf2Sgxs(elfSgxPath, sgxsPath string, heapSize, stackSize, threads uint64) error { + args := []string{ + elfSgxPath, + "--heap-size", strconv.FormatUint(heapSize, 10), + "--stack-size", strconv.FormatUint(stackSize, 10), + "--threads", strconv.FormatUint(threads, 10), + "--output", sgxsPath, + } + + cmd := exec.Command("ftxsgx-elf2sgxs", args...) + return cmd.Run() +} diff --git a/cmd/contract.go b/cmd/contract.go index b0cb39bc..37cf3699 100644 --- a/cmd/contract.go +++ b/cmd/contract.go @@ -10,7 +10,7 @@ import ( "github.com/spf13/cobra" flag "github.com/spf13/pflag" - "gopkg.in/yaml.v2" + "gopkg.in/yaml.v3" "github.com/oasisprotocol/oasis-core/go/common/cbor" "github.com/oasisprotocol/oasis-sdk/client-sdk/go/client" diff --git a/cmd/rofl/build.go b/cmd/rofl/build.go new file mode 100644 index 00000000..1942fe89 --- /dev/null +++ b/cmd/rofl/build.go @@ -0,0 +1,275 @@ +package rofl + +import ( + "crypto/rand" + "crypto/rsa" + "fmt" + "io" + "math/big" + "os" + "time" + + "github.com/spf13/cobra" + flag "github.com/spf13/pflag" + + "github.com/oasisprotocol/oasis-core/go/common/sgx" + "github.com/oasisprotocol/oasis-core/go/common/sgx/sigstruct" + "github.com/oasisprotocol/oasis-core/go/common/version" + "github.com/oasisprotocol/oasis-core/go/runtime/bundle" + "github.com/oasisprotocol/oasis-core/go/runtime/bundle/component" + + "github.com/oasisprotocol/cli/build/cargo" + "github.com/oasisprotocol/cli/build/sgxs" + "github.com/oasisprotocol/cli/cmd/common" + cliConfig "github.com/oasisprotocol/cli/config" +) + +var ( + sgxHeapSize uint64 + sgxStackSize uint64 + sgxThreads uint64 + + outputFn string + + buildCmd = &cobra.Command{ + Use: "build", + Short: "Build a ROFL application", + } + + buildSgxCmd = &cobra.Command{ + Use: "sgx", + Short: "Build an SGX-based Rust ROFL application", + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + cfg := cliConfig.Global() + npa := common.GetNPASelection(cfg) + + if npa.ParaTime == nil { + cobra.CheckErr("no ParaTime selected") + } + + // For SGX we currently only support Rust applications. + + fmt.Println("Building an SGX-based Rust ROFL application...") + + // Obtain package metadata. + pkgMeta, err := cargo.GetMetadata() + if err != nil { + cobra.CheckErr(fmt.Errorf("failed to obtain package metadata: %w", err)) + } + + // Start creating the bundle early so we can fail before building anything. + bnd := &bundle.Bundle{ + Manifest: &bundle.Manifest{ + Name: pkgMeta.Name, + ID: npa.ParaTime.Namespace(), + }, + } + bnd.Manifest.Version, err = version.FromString(pkgMeta.Version) + if err != nil { + cobra.CheckErr(fmt.Errorf("unsupported package version format: %w", err)) + } + + fmt.Printf("Name: %s\n", bnd.Manifest.Name) + fmt.Printf("Version: %s\n", bnd.Manifest.Version) + + // First build for the default target. + fmt.Println("Building ELF binary...") + elfPath, err := cargo.Build(true, "") + if err != nil { + cobra.CheckErr(fmt.Errorf("failed to build ELF binary: %w", err)) + } + + // Then build for the SGX target. + fmt.Println("Building SGXS binary...") + elfSgxPath, err := cargo.Build(true, "x86_64-fortanix-unknown-sgx") + if err != nil { + cobra.CheckErr(fmt.Errorf("failed to build SGXS binary: %w", err)) + } + + sgxsPath := fmt.Sprintf("%s.sgxs", elfSgxPath) + err = sgxs.Elf2Sgxs(elfSgxPath, sgxsPath, sgxHeapSize, sgxStackSize, sgxThreads) + if err != nil { + cobra.CheckErr(fmt.Errorf("failed to generate SGXS binary: %w", err)) + } + + // Compute MRENCLAVE. + var b []byte + if b, err = os.ReadFile(sgxsPath); err != nil { + cobra.CheckErr(fmt.Errorf("failed to read SGXS binary: %w", err)) + } + var enclaveHash sgx.MrEnclave + if err = enclaveHash.FromSgxsBytes(b); err != nil { + cobra.CheckErr(fmt.Errorf("failed to compute MRENCLAVE for SGXS binary: %w", err)) + } + + fmt.Println("Creating ORC bundle...") + + // Create a random 3072-bit RSA signer and prepare SIGSTRUCT. + // TODO: Support a specific signer to be set. + sigKey, err := sgxGenerateKey(rand.Reader) + if err != nil { + cobra.CheckErr(fmt.Errorf("failed to generate signer key: %w", err)) + } + sigStruct := sigstruct.New( + sigstruct.WithBuildDate(time.Now()), + sigstruct.WithSwDefined([4]byte{0, 0, 0, 0}), + sigstruct.WithISVProdID(0), + sigstruct.WithISVSVN(0), + + sigstruct.WithMiscSelect(0), + sigstruct.WithMiscSelectMask(^uint32(0)), + + sigstruct.WithAttributes(sgx.Attributes{ + Flags: sgx.AttributeMode64Bit, + Xfrm: 3, + }), + sigstruct.WithAttributesMask([2]uint64{ + ^uint64(2), + ^uint64(3), + }), + + sigstruct.WithEnclaveHash(enclaveHash), + ) + sigData, err := sigStruct.Sign(sigKey) + if err != nil { + cobra.CheckErr(fmt.Errorf("failed to sign SIGSTRUCT: %w", err)) + } + + // Add the ROFL component. + execName := "app.elf" + sgxsName := "app.sgxs" + sigName := "app.sig" + + comp := bundle.Component{ + Kind: component.ROFL, + Name: pkgMeta.Name, + Executable: execName, + SGX: &bundle.SGXMetadata{ + Executable: sgxsName, + Signature: sigName, + }, + } + bnd.Manifest.Components = append(bnd.Manifest.Components, &comp) + + if err = bnd.Manifest.Validate(); err != nil { + cobra.CheckErr(fmt.Errorf("failed to validate manifest: %w", err)) + } + + // Add all files. + fileMap := map[string]string{ + execName: elfPath, + sgxsName: sgxsPath, + } + for dst, src := range fileMap { + if b, err = os.ReadFile(src); err != nil { + cobra.CheckErr(fmt.Errorf("failed to load asset '%s': %w", src, err)) + } + _ = bnd.Add(dst, b) + } + _ = bnd.Add(sigName, sigData) + + // Write the bundle out. + outFn := fmt.Sprintf("%s.orc", bnd.Manifest.Name) + if outputFn != "" { + outFn = outputFn + } + if err = bnd.Write(outFn); err != nil { + cobra.CheckErr(fmt.Errorf("failed to write output bundle: %w", err)) + } + + fmt.Printf("ROFL app built and bundle written to '%s'.\n", outFn) + }, + } +) + +// sgxGenerateKey generates a 3072-bit RSA key with public exponent 3 as required for SGX. +// +// The code below is adopted from the Go standard library as it is otherwise not possible to +// customize the exponent. +func sgxGenerateKey(random io.Reader) (*rsa.PrivateKey, error) { + priv := new(rsa.PrivateKey) + priv.E = 3 + bits := 3072 + nprimes := 2 + + bigOne := big.NewInt(1) + primes := make([]*big.Int, nprimes) + +NextSetOfPrimes: + for { + todo := bits + // crypto/rand should set the top two bits in each prime. + // Thus each prime has the form + // p_i = 2^bitlen(p_i) × 0.11... (in base 2). + // And the product is: + // P = 2^todo × α + // where α is the product of nprimes numbers of the form 0.11... + // + // If α < 1/2 (which can happen for nprimes > 2), we need to + // shift todo to compensate for lost bits: the mean value of 0.11... + // is 7/8, so todo + shift - nprimes * log2(7/8) ~= bits - 1/2 + // will give good results. + if nprimes >= 7 { + todo += (nprimes - 2) / 5 + } + for i := 0; i < nprimes; i++ { + var err error + primes[i], err = rand.Prime(random, todo/(nprimes-i)) + if err != nil { + return nil, err + } + todo -= primes[i].BitLen() + } + + // Make sure that primes is pairwise unequal. + for i, prime := range primes { + for j := 0; j < i; j++ { + if prime.Cmp(primes[j]) == 0 { + continue NextSetOfPrimes + } + } + } + + n := new(big.Int).Set(bigOne) + totient := new(big.Int).Set(bigOne) + pminus1 := new(big.Int) + for _, prime := range primes { + n.Mul(n, prime) + pminus1.Sub(prime, bigOne) + totient.Mul(totient, pminus1) + } + if n.BitLen() != bits { + // This should never happen for nprimes == 2 because + // crypto/rand should set the top two bits in each prime. + // For nprimes > 2 we hope it does not happen often. + continue NextSetOfPrimes + } + + priv.D = new(big.Int) + e := big.NewInt(int64(priv.E)) + ok := priv.D.ModInverse(e, totient) + + if ok != nil { + priv.Primes = primes + priv.N = n + break + } + } + + priv.Precompute() + return priv, nil +} + +func init() { + sgxFlags := flag.NewFlagSet("", flag.ContinueOnError) + sgxFlags.Uint64Var(&sgxHeapSize, "sgx-heap-size", 512*1024*1024, "SGX enclave heap size") + sgxFlags.Uint64Var(&sgxStackSize, "sgx-stack-size", 2*1024*1024, "SGX enclave stack size") + sgxFlags.Uint64Var(&sgxThreads, "sgx-threads", 32, "SGX enclave maximum number of threads") + sgxFlags.StringVar(&outputFn, "output", "", "output bundle filename") + + buildSgxCmd.Flags().AddFlagSet(common.SelectorNPFlags) + buildSgxCmd.Flags().AddFlagSet(sgxFlags) + + buildCmd.AddCommand(buildSgxCmd) +} diff --git a/cmd/rofl/identity.go b/cmd/rofl/identity.go new file mode 100644 index 00000000..91b8b19b --- /dev/null +++ b/cmd/rofl/identity.go @@ -0,0 +1,76 @@ +package rofl + +import ( + "fmt" + + "github.com/spf13/cobra" + flag "github.com/spf13/pflag" + + "github.com/oasisprotocol/oasis-core/go/common/sgx" + "github.com/oasisprotocol/oasis-core/go/runtime/bundle" + "github.com/oasisprotocol/oasis-core/go/runtime/bundle/component" +) + +var ( + compID string + + identityCmd = &cobra.Command{ + Use: "identity app.orc [--component ID]", + Short: "Show the cryptographic identity of the ROFL app(s) in the specified bundle", + Aliases: []string{"id"}, + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + bundleFn := args[0] + + bnd, err := bundle.Open(bundleFn) + if err != nil { + cobra.CheckErr(fmt.Errorf("failed to open bundle: %w", err)) + } + + var cid component.ID + if compID != "" { + if err = cid.UnmarshalText([]byte(compID)); err != nil { + cobra.CheckErr(fmt.Errorf("malformed component ID: %w", err)) + } + } + + var enclaveID *sgx.EnclaveIdentity + for _, comp := range bnd.Manifest.GetAvailableComponents() { + if comp.Kind != component.ROFL { + continue // Skip non-ROFL components. + } + switch compID { + case "": + // When not specified we use the first ROFL app. + default: + if !comp.Matches(cid) { + continue + } + } + + enclaveID, err = bnd.EnclaveIdentity(comp.ID()) + if err != nil { + cobra.CheckErr(fmt.Errorf("failed to generate enclave identity of '%s': %w", comp.ID(), err)) + } + + data, _ := enclaveID.MarshalText() + fmt.Println(string(data)) + return + } + + switch compID { + case "": + cobra.CheckErr("no ROFL apps found in bundle") + default: + cobra.CheckErr(fmt.Errorf("ROFL app '%s' not found in bundle", compID)) + } + }, + } +) + +func init() { + idFlags := flag.NewFlagSet("", flag.ContinueOnError) + idFlags.StringVar(&compID, "component", "", "optional component ID") + + identityCmd.Flags().AddFlagSet(idFlags) +} diff --git a/cmd/rofl/mgmt.go b/cmd/rofl/mgmt.go new file mode 100644 index 00000000..74fb1df4 --- /dev/null +++ b/cmd/rofl/mgmt.go @@ -0,0 +1,259 @@ +package rofl + +import ( + "context" + "encoding/json" + "fmt" + "os" + + "github.com/spf13/cobra" + flag "github.com/spf13/pflag" + "gopkg.in/yaml.v3" + + "github.com/oasisprotocol/oasis-sdk/client-sdk/go/client" + "github.com/oasisprotocol/oasis-sdk/client-sdk/go/connection" + "github.com/oasisprotocol/oasis-sdk/client-sdk/go/helpers" + "github.com/oasisprotocol/oasis-sdk/client-sdk/go/modules/rofl" + + "github.com/oasisprotocol/cli/cmd/common" + cliConfig "github.com/oasisprotocol/cli/config" +) + +var ( + policyFn string + adminAddress string + + createCmd = &cobra.Command{ + Use: "create ", + Short: "Create a new ROFL application", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + cfg := cliConfig.Global() + npa := common.GetNPASelection(cfg) + txCfg := common.GetTransactionConfig() + policyFn = args[0] + + if npa.Account == nil { + cobra.CheckErr("no accounts configured in your wallet") + } + if npa.ParaTime == nil { + cobra.CheckErr("no ParaTime selected") + } + + policy := loadPolicy(policyFn) + + // When not in offline mode, connect to the given network endpoint. + ctx := context.Background() + var conn connection.Connection + if !txCfg.Offline { + var err error + conn, err = connection.Connect(ctx, npa.Network) + cobra.CheckErr(err) + } + + // Prepare transaction. + tx := rofl.NewCreateTx(nil, &rofl.Create{ + Policy: *policy, + }) + + acc := common.LoadAccount(cfg, npa.AccountName) + sigTx, meta, err := common.SignParaTimeTransaction(ctx, npa, acc, conn, tx, nil) + cobra.CheckErr(err) + + var appID rofl.AppID + if !common.BroadcastOrExportTransaction(ctx, npa.ParaTime, conn, sigTx, meta, &appID) { + return + } + + fmt.Printf("Created ROFL application: %s\n", appID) + }, + } + + updateCmd = &cobra.Command{ + Use: "update --policy --admin
", + Short: "Create a new ROFL application", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + cfg := cliConfig.Global() + npa := common.GetNPASelection(cfg) + txCfg := common.GetTransactionConfig() + rawAppID := args[0] + var appID rofl.AppID + if err := appID.UnmarshalText([]byte(rawAppID)); err != nil { + cobra.CheckErr(fmt.Errorf("malformed ROFL app ID: %w", err)) + } + + if npa.Account == nil { + cobra.CheckErr("no accounts configured in your wallet") + } + if npa.ParaTime == nil { + cobra.CheckErr("no ParaTime selected") + } + + if policyFn == "" || adminAddress == "" { + fmt.Println("You must specify both --policy and --admin.") + return + } + + // When not in offline mode, connect to the given network endpoint. + ctx := context.Background() + var conn connection.Connection + if !txCfg.Offline { + var err error + conn, err = connection.Connect(ctx, npa.Network) + cobra.CheckErr(err) + } + + updateBody := rofl.Update{ + ID: appID, + Policy: *loadPolicy(policyFn), + } + + // Update administrator address. + if adminAddress == "self" { + adminAddress = npa.AccountName + } + var err error + updateBody.Admin, _, err = common.ResolveLocalAccountOrAddress(npa.Network, adminAddress) + if err != nil { + cobra.CheckErr(fmt.Errorf("bad administrator address: %w", err)) + } + + // Prepare transaction. + tx := rofl.NewUpdateTx(nil, &updateBody) + + acc := common.LoadAccount(cfg, npa.AccountName) + sigTx, meta, err := common.SignParaTimeTransaction(ctx, npa, acc, conn, tx, nil) + cobra.CheckErr(err) + + common.BroadcastOrExportTransaction(ctx, npa.ParaTime, conn, sigTx, meta, nil) + }, + } + + removeCmd = &cobra.Command{ + Use: "remove ", + Short: "Remove an existing ROFL application", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + cfg := cliConfig.Global() + npa := common.GetNPASelection(cfg) + txCfg := common.GetTransactionConfig() + rawAppID := args[0] + var appID rofl.AppID + if err := appID.UnmarshalText([]byte(rawAppID)); err != nil { + cobra.CheckErr(fmt.Errorf("malformed ROFL app ID: %w", err)) + } + + if npa.Account == nil { + cobra.CheckErr("no accounts configured in your wallet") + } + if npa.ParaTime == nil { + cobra.CheckErr("no ParaTime selected") + } + + // When not in offline mode, connect to the given network endpoint. + ctx := context.Background() + var conn connection.Connection + if !txCfg.Offline { + var err error + conn, err = connection.Connect(ctx, npa.Network) + cobra.CheckErr(err) + } + + // Prepare transaction. + tx := rofl.NewRemoveTx(nil, &rofl.Remove{ + ID: appID, + }) + + acc := common.LoadAccount(cfg, npa.AccountName) + sigTx, meta, err := common.SignParaTimeTransaction(ctx, npa, acc, conn, tx, nil) + cobra.CheckErr(err) + + common.BroadcastOrExportTransaction(ctx, npa.ParaTime, conn, sigTx, meta, nil) + }, + } + + showCmd = &cobra.Command{ + Use: "show ", + Short: "Show information about a ROFL application", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + cfg := cliConfig.Global() + npa := common.GetNPASelection(cfg) + rawAppID := args[0] + var appID rofl.AppID + if err := appID.UnmarshalText([]byte(rawAppID)); err != nil { + cobra.CheckErr(fmt.Errorf("malformed ROFL app ID: %w", err)) + } + + // Establish connection with the target network. + ctx := context.Background() + conn, err := connection.Connect(ctx, npa.Network) + cobra.CheckErr(err) + + appCfg, err := conn.Runtime(npa.ParaTime).ROFL.App(ctx, client.RoundLatest, appID) + cobra.CheckErr(err) + + fmt.Printf("App ID: %s\n", appCfg.ID) + fmt.Printf("Admin: ") + switch appCfg.Admin { + case nil: + fmt.Printf("none\n") + default: + fmt.Printf("%s\n", *appCfg.Admin) + } + stakedAmnt := helpers.FormatParaTimeDenomination(npa.ParaTime, appCfg.Stake) + fmt.Printf("Staked amount: %s\n", stakedAmnt) + fmt.Printf("Policy:\n") + policyJSON, _ := json.MarshalIndent(appCfg.Policy, " ", " ") + fmt.Printf(" %s\n", string(policyJSON)) + + fmt.Println() + fmt.Printf("=== Instances ===\n") + + appInstances, err := conn.Runtime(npa.ParaTime).ROFL.AppInstances(ctx, client.RoundLatest, appID) + cobra.CheckErr(err) + + if len(appInstances) > 0 { + for _, ai := range appInstances { + fmt.Printf("- RAK: %s\n", ai.RAK) + fmt.Printf(" Node ID: %s\n", ai.NodeID) + fmt.Printf(" Expiration: %d\n", ai.Expiration) + } + } else { + fmt.Println("No registered app instances.") + } + }, + } +) + +func loadPolicy(fn string) *rofl.AppAuthPolicy { + // Load app policy. + rawPolicy, err := os.ReadFile(fn) + cobra.CheckErr(err) + + // Parse policy. + var policy rofl.AppAuthPolicy + if err = yaml.Unmarshal(rawPolicy, &policy); err != nil { + cobra.CheckErr(fmt.Errorf("malformed ROFL app policy: %w", err)) + } + return &policy +} + +func init() { + updateFlags := flag.NewFlagSet("", flag.ContinueOnError) + updateFlags.StringVar(&policyFn, "policy", "", "set the ROFL application policy") + updateFlags.StringVar(&adminAddress, "admin", "", "set the administrator address") + + createCmd.Flags().AddFlagSet(common.SelectorFlags) + createCmd.Flags().AddFlagSet(common.RuntimeTxFlags) + + updateCmd.Flags().AddFlagSet(common.SelectorFlags) + updateCmd.Flags().AddFlagSet(common.RuntimeTxFlags) + updateCmd.Flags().AddFlagSet(updateFlags) + + removeCmd.Flags().AddFlagSet(common.SelectorFlags) + removeCmd.Flags().AddFlagSet(common.RuntimeTxFlags) + + showCmd.Flags().AddFlagSet(common.SelectorFlags) +} diff --git a/cmd/rofl/rofl.go b/cmd/rofl/rofl.go new file mode 100644 index 00000000..02c675dd --- /dev/null +++ b/cmd/rofl/rofl.go @@ -0,0 +1,21 @@ +package rofl + +import ( + "github.com/spf13/cobra" +) + +var Cmd = &cobra.Command{ + Use: "rofl", + Short: "ROFL app management", + Aliases: []string{"r"}, +} + +func init() { + Cmd.AddCommand(createCmd) + Cmd.AddCommand(updateCmd) + Cmd.AddCommand(removeCmd) + Cmd.AddCommand(showCmd) + Cmd.AddCommand(trustRootCmd) + Cmd.AddCommand(buildCmd) + Cmd.AddCommand(identityCmd) +} diff --git a/cmd/rofl/trust_root.go b/cmd/rofl/trust_root.go new file mode 100644 index 00000000..47de9b58 --- /dev/null +++ b/cmd/rofl/trust_root.go @@ -0,0 +1,55 @@ +package rofl + +import ( + "context" + "fmt" + + "github.com/spf13/cobra" + + "github.com/oasisprotocol/oasis-sdk/client-sdk/go/connection" + + "github.com/oasisprotocol/cli/cmd/common" + cliConfig "github.com/oasisprotocol/cli/config" +) + +var trustRootCmd = &cobra.Command{ + Use: "trust-root", + Short: "Show a recent trust root for a ROFL application", + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + cfg := cliConfig.Global() + npa := common.GetNPASelection(cfg) + + if npa.ParaTime == nil { + cobra.CheckErr("no ParaTime selected") + } + + // Establish connection with the target network. + ctx := context.Background() + conn, err := connection.Connect(ctx, npa.Network) + cobra.CheckErr(err) + + // Fetch latest consensus block. + height, err := common.GetActualHeight( + ctx, + conn.Consensus(), + ) + cobra.CheckErr(err) + + blk, err := conn.Consensus().GetBlock(ctx, height) + cobra.CheckErr(err) + + // TODO: Support different output formats. + fmt.Printf("TrustRoot {\n") + fmt.Printf(" height: %d,\n", height) + fmt.Printf(" hash: \"%s\".into(),\n", blk.Hash) + fmt.Printf(" runtime_id: \"%s\".into(),\n", npa.ParaTime.ID) + fmt.Printf(" chain_context: \"%s\".to_string(),\n", npa.Network.ChainContext) + fmt.Printf("}\n") + }, +} + +func init() { + trustRootCmd.Flags().AddFlagSet(common.SelectorNPFlags) + trustRootCmd.Flags().AddFlagSet(common.HeightFlag) +} diff --git a/cmd/root.go b/cmd/root.go index c324b2a0..9fd244ab 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -13,6 +13,7 @@ import ( "github.com/oasisprotocol/cli/cmd/account" "github.com/oasisprotocol/cli/cmd/network" "github.com/oasisprotocol/cli/cmd/paratime" + "github.com/oasisprotocol/cli/cmd/rofl" "github.com/oasisprotocol/cli/cmd/wallet" "github.com/oasisprotocol/cli/config" "github.com/oasisprotocol/cli/version" @@ -104,4 +105,5 @@ func init() { rootCmd.AddCommand(addressBookCmd) rootCmd.AddCommand(contractCmd) rootCmd.AddCommand(txCmd) + rootCmd.AddCommand(rofl.Cmd) } diff --git a/examples/setup/first-run.out b/examples/setup/first-run.out index 9d3e41a8..8bf84058 100644 --- a/examples/setup/first-run.out +++ b/examples/setup/first-run.out @@ -11,6 +11,7 @@ Available Commands: help Help about any command network Consensus layer operations paratime ParaTime layer operations + rofl ROFL app management transaction Raw transaction operations wallet Manage accounts in the local wallet diff --git a/go.mod b/go.mod index 114ac774..74245b55 100644 --- a/go.mod +++ b/go.mod @@ -30,7 +30,7 @@ require ( github.com/tyler-smith/go-bip39 v1.1.0 github.com/zondax/ledger-go v0.14.3 golang.org/x/crypto v0.26.0 - gopkg.in/yaml.v2 v2.4.0 + gopkg.in/yaml.v3 v3.0.1 ) require ( @@ -146,7 +146,6 @@ require ( google.golang.org/protobuf v1.33.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect lukechampine.com/blake3 v1.2.2 // indirect rsc.io/tmplfunc v0.0.3 // indirect )