Skip to content

Commit

Permalink
wip: fork
Browse files Browse the repository at this point in the history
Signed-off-by: gfanton <[email protected]>
  • Loading branch information
gfanton committed Dec 20, 2024
1 parent e01d185 commit 9586dfe
Show file tree
Hide file tree
Showing 10 changed files with 23,936 additions and 212 deletions.
119 changes: 119 additions & 0 deletions gno.land/pkg/integration/fork_config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package integration

import (
"bufio"
"bytes"
"context"
"encoding/json"
"fmt"
"os"
"os/exec"

"github.com/gnolang/gno/tm2/pkg/amino"
tmcfg "github.com/gnolang/gno/tm2/pkg/bft/config"
bft "github.com/gnolang/gno/tm2/pkg/bft/types"
)

type MarshalableGenesisDoc bft.GenesisDoc

func NewMarshalableGenesisDoc(doc *bft.GenesisDoc) *MarshalableGenesisDoc {
m := MarshalableGenesisDoc(*doc)
return &m
}

func (m *MarshalableGenesisDoc) MarshalJSON() ([]byte, error) {
doc := (*bft.GenesisDoc)(m)
return amino.MarshalJSON(doc)
}

func (m *MarshalableGenesisDoc) UnmarshalJSON(data []byte) (err error) {
doc, err := bft.GenesisDocFromJSON(data)
if err != nil {
return err
}

*m = MarshalableGenesisDoc(*doc)
return
}

// Function to cast back to the original bft.GenesisDoc
func (m *MarshalableGenesisDoc) ToGenesisDoc() *bft.GenesisDoc {
return (*bft.GenesisDoc)(m)
}

type ForkConfig struct {
RootDir string `json:"rootdir"`
Genesis *MarshalableGenesisDoc `json:"genesis"`
TMConfig *tmcfg.Config `json:"tm"`
}

// ExecuteForkBinary runs the binary at the given path with the provided configuration.
// It marshals the configuration to JSON and passes it to the binary via stdin.
// The function waits for "READY:<address>" on stdout and returns the address if successful,
// or kills the process and returns an error if "READY" is not received within 10 seconds.
func ExecuteForkBinary(ctx context.Context, binaryPath string, cfg *ForkConfig) (string, *exec.Cmd, error) {
// Marshal the configuration to JSON
configData, err := json.Marshal(cfg)
if err != nil {
return "", nil, fmt.Errorf("failed to marshal config to JSON: %w", err)
}

// Create the command to execute the binary
cmd := exec.Command(binaryPath)
cmd.Env = os.Environ()

// Set the standard input to the JSON data
cmd.Stdin = bytes.NewReader(configData)

// Create pipes for stdout and stderr
stdoutPipe, err := cmd.StdoutPipe()
if err != nil {
return "", nil, fmt.Errorf("failed to create stdout pipe: %w", err)
}

cmd.Stderr = os.Stderr

// Start the command
if err := cmd.Start(); err != nil {
return "", nil, fmt.Errorf("failed to start command: %w", err)
}

readyChan := make(chan error, 1)
var address string

// Goroutine to read stdout and check for "READY"
go func() {
var scanned bool
scanner := bufio.NewScanner(stdoutPipe)
for scanner.Scan() {
line := scanner.Text()
fmt.Println(line) // Print each line to stdout for logging
if scanned {
continue
}
if _, err := fmt.Sscanf(line, "READY:%s", &address); err == nil {
readyChan <- nil
scanned = true
}
}
if err := scanner.Err(); err != nil {
readyChan <- fmt.Errorf("error reading stdout: %w", err)
} else {
readyChan <- fmt.Errorf("process exited without 'READY'")
}
}()

// Wait for either the "READY" signal or a timeout
select {
case err := <-readyChan:
if err != nil {
cmd.Process.Kill()
return "", cmd, err
}
case <-ctx.Done():
cmd.Process.Kill()
return "", cmd, ctx.Err()
}

return address, cmd, nil
}
47 changes: 47 additions & 0 deletions gno.land/pkg/integration/fork_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package integration

import (
"context"
"fmt"
"path/filepath"
"testing"
"time"

"github.com/gnolang/gno/gnovm/pkg/gnoenv"
"github.com/gnolang/gno/tm2/pkg/bft/rpc/client"
"github.com/stretchr/testify/require"
)

func TestForkGnoland(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()

tmpdir := t.TempDir()

gnoRootDir := gnoenv.RootDir()

gnolandBuildDir := filepath.Join(tmpdir, "build")
gnolandBin := filepath.Join(gnolandBuildDir, "gnoland")
err := buildGnoland(t, gnoRootDir, gnolandBin)
require.NoError(t, err)

cfg := TestingMinimalNodeConfig(gnoRootDir)

gnoenv.RootDir()
remoteAddr, cmd, err := ExecuteForkBinary(ctx, gnolandBin, &ForkConfig{
RootDir: gnoRootDir,
TMConfig: cfg.TMConfig,
Genesis: NewMarshalableGenesisDoc(cfg.Genesis),
})
require.NoError(t, err)

defer cmd.Process.Kill()

cli, err := client.NewHTTPClient(remoteAddr)
require.NoError(t, err)

info, err := cli.ABCIInfo()
require.NoError(t, err)

fmt.Println(info)
}
86 changes: 86 additions & 0 deletions gno.land/pkg/integration/forknode/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package main

import (
"encoding/json"
"fmt"
"io"
"log/slog"
"os"
"slices"
"time"

"github.com/gnolang/gno/gno.land/pkg/gnoland"
"github.com/gnolang/gno/gno.land/pkg/integration"
bft "github.com/gnolang/gno/tm2/pkg/bft/types"
)

func ForkableNode(cfg *integration.ForkConfig) error {
logger := slog.New(slog.NewTextHandler(io.Discard, nil))

nodecfg := integration.TestingMinimalNodeConfig(cfg.RootDir)
pv := nodecfg.PrivValidator.GetPubKey()
nodecfg.TMConfig = cfg.TMConfig
nodecfg.Genesis = cfg.Genesis.ToGenesisDoc()
nodecfg.Genesis.Validators = []bft.GenesisValidator{
{
Address: pv.Address(),
PubKey: pv,
Power: 10,
Name: "self",
},
}

node, err := gnoland.NewInMemoryNode(logger, nodecfg)
if err != nil {
return fmt.Errorf("failed to create new in-memory node: %w", err)
}

err = node.Start()
if err != nil {
return fmt.Errorf("failed to start node: %w", err)
}

ourAddress := nodecfg.PrivValidator.GetPubKey().Address()
isValidator := slices.ContainsFunc(nodecfg.Genesis.Validators, func(val bft.GenesisValidator) bool {
return val.Address == ourAddress
})

// Wait for first block if we are a validator.
// If we are not a validator, we don't produce blocks, so node.Ready() hangs.
if isValidator {
select {
case <-node.Ready():
fmt.Printf("READY:%s\n", node.Config().RPC.ListenAddress)
case <-time.After(time.Second * 10):
return fmt.Errorf("timeout while waiting for the node to start")
}
} else {
fmt.Printf("READY:%s\n", node.Config().RPC.ListenAddress)
}

// Keep the function running indefinitely if no errors occur
select {}
}

func main() {
// Read the configuration from standard input
configData, err := io.ReadAll(os.Stdin)
if err != nil {
fmt.Fprintf(os.Stdout, "Error reading stdin: %v\n", err)
os.Exit(1)
}

// Unmarshal the JSON configuration
var cfg integration.ForkConfig
err = json.Unmarshal(configData, &cfg)
if err != nil {
fmt.Fprintf(os.Stdout, "Error unmarshaling JSON: %v\n", err)
os.Exit(1)
}

// Call the ForkableNode function with the parsed configuration
if err := ForkableNode(&cfg); err != nil {
fmt.Fprintf(os.Stdout, "Error running ForkableNode: %v\n", err)
os.Exit(1)
}
}
Loading

0 comments on commit 9586dfe

Please sign in to comment.