Skip to content

Commit

Permalink
fix(gnodev): timestamp issue on reload (gnolang#2943)
Browse files Browse the repository at this point in the history
- [x] depends on gnolang#2941  

resolve gnolang#1509  

This PR addresses the timestamp issue on gnodev by implementing the
`MetadataTX` changes from gnolang#2941. Timestamps will now be correctly
handled for `Reload` and `import`/`export`. For `Reset`, the timestamp
will be updated to the current time.

cc @zivkovicmilos @thehowl  

#### ~TODOs~ 
-  [x] test replays (I've only tested it manually)  


<details><summary>Contributors' checklist...</summary>

- [ ] Added new tests, or not needed, or not feasible
- [ ] Provided an example (e.g. screenshot) to aid review or the PR is
self-explanatory
- [ ] Updated the official documentation or not needed
- [ ] No breaking changes were made, or a `BREAKING CHANGE: xxx` message
was included in the description
- [ ] Added references to related issues and PRs
- [ ] Provided any useful hints for running manual tests
- [ ] Added new benchmarks to [generated
graphs](https://gnoland.github.io/benchmarks), if any. More info
[here](https://github.com/gnolang/gno/blob/master/.benchmarks/README.md).
</details>

---------

Signed-off-by: gfanton <[email protected]>
  • Loading branch information
gfanton authored and r3v4s committed Dec 10, 2024
1 parent 6322bfa commit c07f2ae
Show file tree
Hide file tree
Showing 5 changed files with 247 additions and 30 deletions.
2 changes: 1 addition & 1 deletion contribs/gnodev/cmd/gnodev/setup_node.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ func setupDevNode(
nodeConfig.InitialTxs[index] = nodeTx
}

logger.Info("genesis file loaded", "path", devCfg.genesisFile, "txs", len(nodeConfig.InitialTxs))
logger.Info("genesis file loaded", "path", devCfg.genesisFile, "txs", len(stateTxs))
}

return gnodev.NewDevNode(ctx, nodeConfig)
Expand Down
22 changes: 16 additions & 6 deletions contribs/gnodev/pkg/dev/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"path/filepath"
"strings"
"sync"
"time"
"unicode"

"github.com/gnolang/gno/contribs/gnodev/pkg/emitter"
Expand Down Expand Up @@ -84,6 +85,9 @@ type Node struct {
// keep track of number of loaded package to be able to skip them on restore
loadedPackages int

// track starting time for genesis
startTime time.Time

// state
initialState, state []gnoland.TxWithMetadata
currentStateIndex int
Expand All @@ -97,7 +101,8 @@ func NewDevNode(ctx context.Context, cfg *NodeConfig) (*Node, error) {
return nil, fmt.Errorf("unable map pkgs list: %w", err)
}

pkgsTxs, err := mpkgs.Load(DefaultFee)
startTime := time.Now()
pkgsTxs, err := mpkgs.Load(DefaultFee, startTime)
if err != nil {
return nil, fmt.Errorf("unable to load genesis packages: %w", err)
}
Expand All @@ -110,6 +115,7 @@ func NewDevNode(ctx context.Context, cfg *NodeConfig) (*Node, error) {
pkgs: mpkgs,
logger: cfg.Logger,
loadedPackages: len(pkgsTxs),
startTime: startTime,
state: cfg.InitialTxs,
initialState: cfg.InitialTxs,
currentStateIndex: len(cfg.InitialTxs),
Expand Down Expand Up @@ -173,9 +179,10 @@ func (n *Node) getBlockTransactions(blockNum uint64) ([]gnoland.TxWithMetadata,

txs := make([]gnoland.TxWithMetadata, len(b.Block.Data.Txs))
for i, encodedTx := range b.Block.Data.Txs {
// fallback on std tx
var tx std.Tx
if unmarshalErr := amino.Unmarshal(encodedTx, &tx); unmarshalErr != nil {
return nil, fmt.Errorf("unable to unmarshal amino tx, %w", unmarshalErr)
return nil, fmt.Errorf("unable to unmarshal tx: %w", unmarshalErr)
}

txs[i] = gnoland.TxWithMetadata{
Expand Down Expand Up @@ -268,8 +275,11 @@ func (n *Node) Reset(ctx context.Context) error {
return fmt.Errorf("unable to stop the node: %w", err)
}

// Reset starting time
startTime := time.Now()

// Generate a new genesis state based on the current packages
pkgsTxs, err := n.pkgs.Load(DefaultFee)
pkgsTxs, err := n.pkgs.Load(DefaultFee, startTime)
if err != nil {
return fmt.Errorf("unable to load pkgs: %w", err)
}
Expand All @@ -289,6 +299,7 @@ func (n *Node) Reset(ctx context.Context) error {

n.loadedPackages = len(pkgsTxs)
n.currentStateIndex = len(n.initialState)
n.startTime = startTime
n.emitter.Emit(&events.Reset{})
return nil
}
Expand Down Expand Up @@ -358,7 +369,6 @@ func (n *Node) getBlockStoreState(ctx context.Context) ([]gnoland.TxWithMetadata
genesis := n.GenesisDoc().AppState.(gnoland.GnoGenesisState)

initialTxs := genesis.Txs[n.loadedPackages:] // ignore previously loaded packages

state := append([]gnoland.TxWithMetadata{}, initialTxs...)

lastBlock := n.getLatestBlockNumber()
Expand Down Expand Up @@ -397,7 +407,7 @@ func (n *Node) rebuildNodeFromState(ctx context.Context) error {
// If NoReplay is true, simply reset the node to its initial state
n.logger.Warn("replay disabled")

txs, err := n.pkgs.Load(DefaultFee)
txs, err := n.pkgs.Load(DefaultFee, n.startTime)
if err != nil {
return fmt.Errorf("unable to load pkgs: %w", err)
}
Expand All @@ -413,7 +423,7 @@ func (n *Node) rebuildNodeFromState(ctx context.Context) error {
}

// Load genesis packages
pkgsTxs, err := n.pkgs.Load(DefaultFee)
pkgsTxs, err := n.pkgs.Load(DefaultFee, n.startTime)
if err != nil {
return fmt.Errorf("unable to load pkgs: %w", err)
}
Expand Down
2 changes: 1 addition & 1 deletion contribs/gnodev/pkg/dev/node_state.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ func (n *Node) MoveBy(ctx context.Context, x int) error {
}

// Load genesis packages
pkgsTxs, err := n.pkgs.Load(DefaultFee)
pkgsTxs, err := n.pkgs.Load(DefaultFee, n.startTime)
if err != nil {
return fmt.Errorf("unable to load pkgs: %w", err)
}
Expand Down
217 changes: 209 additions & 8 deletions contribs/gnodev/pkg/dev/node_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ package dev

import (
"context"
"encoding/json"
"os"
"path/filepath"
"testing"
"time"

mock "github.com/gnolang/gno/contribs/gnodev/internal/mock"

Expand All @@ -15,8 +17,10 @@ import (
"github.com/gnolang/gno/gno.land/pkg/sdk/vm"
"github.com/gnolang/gno/gnovm/pkg/gnoenv"
core_types "github.com/gnolang/gno/tm2/pkg/bft/rpc/core/types"
"github.com/gnolang/gno/tm2/pkg/bft/types"
"github.com/gnolang/gno/tm2/pkg/crypto"
"github.com/gnolang/gno/tm2/pkg/crypto/keys"
tm2events "github.com/gnolang/gno/tm2/pkg/events"
"github.com/gnolang/gno/tm2/pkg/log"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -221,6 +225,191 @@ func Render(_ string) string { return str }
assert.Equal(t, mock.EvtNull, emitter.NextEvent().Type())
}

func TestTxTimestampRecover(t *testing.T) {
const (
// foo package
foobarGnoMod = "module gno.land/r/dev/foo\n"
fooFile = `package foo
import (
"strconv"
"strings"
"time"
)
var times = []time.Time{
time.Now(), // Evaluate at genesis
}
func SpanTime() {
times = append(times, time.Now())
}
func Render(_ string) string {
var strs strings.Builder
strs.WriteRune('[')
for i, t := range times {
if i > 0 {
strs.WriteRune(',')
}
strs.WriteString(strconv.Itoa(int(t.UnixNano())))
}
strs.WriteRune(']')
return strs.String()
}
`
)

// Add a hard deadline of 20 seconds to avoid potential deadlock and fail early
ctx, cancel := context.WithTimeout(context.Background(), time.Second*20)
defer cancel()

parseJSONTimesList := func(t *testing.T, render string) []time.Time {
t.Helper()

var times []time.Time
var nanos []int64

err := json.Unmarshal([]byte(render), &nanos)
require.NoError(t, err)

for _, nano := range nanos {
sec, nsec := nano/int64(time.Second), nano%int64(time.Second)
times = append(times, time.Unix(sec, nsec))
}

return times
}

// Generate package foo
foopkg := generateTestingPackage(t, "gno.mod", foobarGnoMod, "foo.gno", fooFile)

// Call NewDevNode with no package should work
cfg := createDefaultTestingNodeConfig(foopkg)

// XXX(gfanton): Setting this to `false` somehow makes the time block
// drift from the time spanned by the VM.
cfg.TMConfig.Consensus.SkipTimeoutCommit = false
cfg.TMConfig.Consensus.TimeoutCommit = 500 * time.Millisecond
cfg.TMConfig.Consensus.TimeoutPropose = 100 * time.Millisecond
cfg.TMConfig.Consensus.CreateEmptyBlocks = true

node, emitter := newTestingDevNodeWithConfig(t, cfg)

// We need to make sure that blocks are separated by at least 1 second
// (minimal time between blocks). We can ensure this by listening for
// new blocks and comparing timestamps
cc := make(chan types.EventNewBlock)
node.Node.EventSwitch().AddListener("test-timestamp", func(evt tm2events.Event) {
newBlock, ok := evt.(types.EventNewBlock)
if !ok {
return
}

select {
case cc <- newBlock:
default:
}
})

// wait for first block for reference
var refHeight, refTimestamp int64

select {
case <-ctx.Done():
require.FailNow(t, ctx.Err().Error())
case res := <-cc:
refTimestamp = res.Block.Time.Unix()
refHeight = res.Block.Height
}

// number of span to process
const nevents = 3

// Span multiple time
for i := 0; i < nevents; i++ {
t.Logf("waiting for a bock greater than height(%d) and unix(%d)", refHeight, refTimestamp)
for {
var block types.EventNewBlock
select {
case <-ctx.Done():
require.FailNow(t, ctx.Err().Error())
case block = <-cc:
}

t.Logf("got a block height(%d) and unix(%d)",
block.Block.Height, block.Block.Time.Unix())

// Ensure we consume every block before tx block
if refHeight >= block.Block.Height {
continue
}

// Ensure new block timestamp is before previous reference timestamp
if newRefTimestamp := block.Block.Time.Unix(); newRefTimestamp > refTimestamp {
refTimestamp = newRefTimestamp
break // break the loop
}
}

t.Logf("found a valid block(%d)! continue", refHeight)

// Span a new time
msg := vm.MsgCall{
PkgPath: "gno.land/r/dev/foo",
Func: "SpanTime",
}

res, err := testingCallRealm(t, node, msg)

require.NoError(t, err)
require.NoError(t, res.CheckTx.Error)
require.NoError(t, res.DeliverTx.Error)
assert.Equal(t, emitter.NextEvent().Type(), events.EvtTxResult)

// Set the new height from the tx as reference
refHeight = res.Height
}

// Render JSON times list
render, err := testingRenderRealm(t, node, "gno.land/r/dev/foo")
require.NoError(t, err)

// Parse times list
timesList1 := parseJSONTimesList(t, render)
t.Logf("list of times: %+v", timesList1)

// Ensure times are correctly expending.
for i, t2 := range timesList1 {
if i == 0 {
continue
}

t1 := timesList1[i-1]
require.Greater(t, t2.UnixNano(), t1.UnixNano())
}

// Reload the node
err = node.Reload(context.Background())
require.NoError(t, err)
assert.Equal(t, emitter.NextEvent().Type(), events.EvtReload)

// Fetch time list again from render
render, err = testingRenderRealm(t, node, "gno.land/r/dev/foo")
require.NoError(t, err)

timesList2 := parseJSONTimesList(t, render)

// Times list should be identical from the orignal list
require.Len(t, timesList2, len(timesList1))
for i := 0; i < len(timesList1); i++ {
t1nsec, t2nsec := timesList1[i].UnixNano(), timesList2[i].UnixNano()
assert.Equal(t, t1nsec, t2nsec,
"comparing times1[%d](%d) == times2[%d](%d)", i, t1nsec, i, t2nsec)
}
}

func testingRenderRealm(t *testing.T, node *Node, rlmpath string) (string, error) {
t.Helper()

Expand Down Expand Up @@ -285,25 +474,37 @@ func generateTestingPackage(t *testing.T, nameFile ...string) PackagePath {
}
}

func createDefaultTestingNodeConfig(pkgslist ...PackagePath) *NodeConfig {
cfg := DefaultNodeConfig(gnoenv.RootDir())
cfg.PackagesPathList = pkgslist
return cfg
}

func newTestingDevNode(t *testing.T, pkgslist ...PackagePath) (*Node, *mock.ServerEmitter) {
t.Helper()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

logger := log.NewTestingLogger(t)
cfg := createDefaultTestingNodeConfig(pkgslist...)
return newTestingDevNodeWithConfig(t, cfg)
}

func newTestingDevNodeWithConfig(t *testing.T, cfg *NodeConfig) (*Node, *mock.ServerEmitter) {
t.Helper()

ctx, cancel := context.WithCancel(context.Background())
logger := log.NewTestingLogger(t)
emitter := &mock.ServerEmitter{}

// Call NewDevNode with no package should work
cfg := DefaultNodeConfig(gnoenv.RootDir())
cfg.PackagesPathList = pkgslist
cfg.Emitter = emitter
cfg.Logger = logger

node, err := NewDevNode(ctx, cfg)
require.NoError(t, err)
assert.Len(t, node.ListPkgs(), len(pkgslist))
assert.Len(t, node.ListPkgs(), len(cfg.PackagesPathList))

t.Cleanup(func() { node.Close() })
t.Cleanup(func() {
node.Close()
cancel()
})

return node, emitter
}
Expand Down
Loading

0 comments on commit c07f2ae

Please sign in to comment.