Skip to content
Closed
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
3 changes: 3 additions & 0 deletions gno.land/pkg/gnoland/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,9 @@ func (cfg InitChainerConfig) loadAppState(ctx sdk.Context, appState any) ([]abci
// which only the timestamp information is modified
header := ctx.BlockHeader().(*bft.Header).Copy()
header.Time = time.Unix(metadata.Timestamp, 0)
if metadata.BlockHeight > 0 {
header.Height = metadata.BlockHeight
}
Comment thread
aeddi marked this conversation as resolved.

// Save the modified header
return ctx.WithBlockHeader(header)
Expand Down
265 changes: 265 additions & 0 deletions gno.land/pkg/gnoland/app_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package gnoland

import (
"bytes"
"context"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"testing"
Expand Down Expand Up @@ -481,6 +483,269 @@ func TestInitChainer_MetadataTxs(t *testing.T) {
}
}

func TestInitChainer_MetadataBlockHeight(t *testing.T) {
var (
db = memdb.NewMemDB()
key = getDummyKey(t)
chainID = "test"
path = "gno.land/r/demo/heighttx"
body = `package heighttx

import "chain/runtime"

// Height is initialized on deployment (genesis)
var h int64 = runtime.ChainHeight()

// GetH returns the height that was saved from genesis
func GetH(cur realm) int64 { return h }
`
)

// Create a fresh app instance
app, err := NewAppWithOptions(TestAppOptions(db))
require.NoError(t, err)

msg := vm.MsgAddPackage{
Creator: key.PubKey().Address(),
Package: &std.MemPackage{
Name: "heighttx",
Path: path,
Files: []*std.MemFile{
{
Name: "file.gno",
Body: body,
},
{
Name: "gnomod.toml",
Body: gnolang.GenGnoModLatest(path),
},
},
},
MaxDeposit: nil,
}

tx := createAndSignTx(t, []std.Msg{msg}, chainID, key)

var expectedHeight int64 = 42

// Run InitChain with metadata containing BlockHeight
app.InitChain(abci.RequestInitChain{
ChainID: chainID,
Time: time.Now(),
ConsensusParams: &abci.ConsensusParams{
Block: defaultBlockParams(),
Validator: &abci.ValidatorParams{
PubKeyTypeURLs: []string{},
},
},
AppState: GnoGenesisState{
Txs: []TxWithMetadata{
{
Tx: tx,
Metadata: &GnoTxMetadata{
Timestamp: time.Now().Unix(),
BlockHeight: expectedHeight,
},
},
},
Balances: []Balance{
{
Address: key.PubKey().Address(),
Amount: std.NewCoins(std.NewCoin("ugnot", 20_000_000)),
},
},
Auth: auth.DefaultGenesisState(),
Bank: bank.DefaultGenesisState(),
VM: vm.DefaultGenesisState(),
},
})

// Call GetH to verify the block height was set during genesis
callMsg := vm.MsgCall{
Caller: key.PubKey().Address(),
PkgPath: path,
Func: "GetH",
}

tx = createAndSignTx(t, []std.Msg{callMsg}, chainID, key)
marshalledTx, err := amino.Marshal(tx)
require.NoError(t, err)

resp := app.DeliverTx(abci.RequestDeliverTx{
Tx: marshalledTx,
})

require.True(t, resp.IsOK())
assert.Contains(
t,
string(resp.Data),
fmt.Sprintf("(%d int64)", expectedHeight),
)
}

func TestGenesisMetadataRoundTrip(t *testing.T) {
t.Parallel()

var (
key = getDummyKey(t)
chainID = "test"

originalTimestamp = time.Now().Unix()
originalBlockHeight = int64(99)
originalChainID = "test-chain-42"
)

// Build two TxWithMetadata entries: one with all fields, one with only Timestamp (backward compat)
path1 := "gno.land/r/demo/roundtrip1"
body1 := `package roundtrip1

import (
"time"
"chain/runtime"
)

var t time.Time = time.Now()
var h int64 = runtime.ChainHeight()

func GetT(cur realm) int64 { return t.Unix() }
func GetH(cur realm) int64 { return h }
`
msg1 := vm.MsgAddPackage{
Creator: key.PubKey().Address(),
Package: &std.MemPackage{
Name: "roundtrip1",
Path: path1,
Files: []*std.MemFile{
{Name: "file.gno", Body: body1},
{Name: "gnomod.toml", Body: gnolang.GenGnoModLatest(path1)},
},
},
MaxDeposit: nil,
}
tx1 := createAndSignTx(t, []std.Msg{msg1}, chainID, key)

path2 := "gno.land/r/demo/roundtrip2"
body2 := `package roundtrip2

import "time"

var t time.Time = time.Now()

func GetT(cur realm) int64 { return t.Unix() }
`
msg2 := vm.MsgAddPackage{
Creator: key.PubKey().Address(),
Package: &std.MemPackage{
Name: "roundtrip2",
Path: path2,
Files: []*std.MemFile{
{Name: "file.gno", Body: body2},
{Name: "gnomod.toml", Body: gnolang.GenGnoModLatest(path2)},
},
},
MaxDeposit: nil,
}
tx2 := createAndSignTx(t, []std.Msg{msg2}, chainID, key)

fullMetadataTx := TxWithMetadata{
Tx: tx1,
Metadata: &GnoTxMetadata{
Timestamp: originalTimestamp,
BlockHeight: originalBlockHeight,
ChainID: originalChainID,
},
}

// Backward compat: only Timestamp, no BlockHeight or ChainID
partialMetadataTx := TxWithMetadata{
Tx: tx2,
Metadata: &GnoTxMetadata{
Timestamp: originalTimestamp,
},
}

// --- Part 1: JSONL round-trip ---

var buf bytes.Buffer
for _, txm := range []TxWithMetadata{fullMetadataTx, partialMetadataTx} {
line, err := amino.MarshalJSON(txm)
require.NoError(t, err)
buf.Write(line)
buf.WriteByte('\n')
}

tmpFile := filepath.Join(t.TempDir(), "txs.jsonl")
require.NoError(t, os.WriteFile(tmpFile, buf.Bytes(), 0o644))

readTxs, err := ReadGenesisTxs(context.Background(), tmpFile)
require.NoError(t, err)
require.Len(t, readTxs, 2)

// Verify full metadata preserved
require.NotNil(t, readTxs[0].Metadata)
require.Equal(t, originalTimestamp, readTxs[0].Metadata.Timestamp)
require.Equal(t, originalBlockHeight, readTxs[0].Metadata.BlockHeight)
require.Equal(t, originalChainID, readTxs[0].Metadata.ChainID)

// Verify backward compat: partial metadata has zero-value BlockHeight and empty ChainID
require.NotNil(t, readTxs[1].Metadata)
require.Equal(t, originalTimestamp, readTxs[1].Metadata.Timestamp)
require.Equal(t, int64(0), readTxs[1].Metadata.BlockHeight)
require.Equal(t, "", readTxs[1].Metadata.ChainID)

// --- Part 2: Genesis replay with full metadata ---

db := memdb.NewMemDB()
app, err := NewAppWithOptions(TestAppOptions(db))
require.NoError(t, err)

app.InitChain(abci.RequestInitChain{
ChainID: chainID,
Time: time.Now(),
ConsensusParams: &abci.ConsensusParams{
Block: defaultBlockParams(),
Validator: &abci.ValidatorParams{
PubKeyTypeURLs: []string{},
},
},
AppState: GnoGenesisState{
Txs: readTxs,
Balances: []Balance{{Address: key.PubKey().Address(), Amount: std.NewCoins(std.NewCoin("ugnot", 20_000_000))}},
Auth: auth.DefaultGenesisState(),
Bank: bank.DefaultGenesisState(),
VM: vm.DefaultGenesisState(),
},
})

// Verify timestamp was applied (via realm 1)
callMsg1 := vm.MsgCall{
Caller: key.PubKey().Address(),
PkgPath: path1,
Func: "GetT",
}
callTx1 := createAndSignTx(t, []std.Msg{callMsg1}, chainID, key)
marshalledTx1, err := amino.Marshal(callTx1)
require.NoError(t, err)

resp1 := app.DeliverTx(abci.RequestDeliverTx{Tx: marshalledTx1})
require.True(t, resp1.IsOK(), "DeliverTx failed: %s", resp1.Log)
assert.Contains(t, string(resp1.Data), fmt.Sprintf("(%d int64)", originalTimestamp))

// Verify block height was applied (via realm 1, which was deployed with full metadata)
callMsg2 := vm.MsgCall{
Caller: key.PubKey().Address(),
PkgPath: path1,
Func: "GetH",
}
callTx2 := createAndSignTx(t, []std.Msg{callMsg2}, chainID, key)
marshalledTx2, err := amino.Marshal(callTx2)
require.NoError(t, err)

resp2 := app.DeliverTx(abci.RequestDeliverTx{Tx: marshalledTx2})
require.True(t, resp2.IsOK(), "DeliverTx failed: %s", resp2.Log)
assert.Contains(t, string(resp2.Data), fmt.Sprintf("(%d int64)", originalBlockHeight))
}

func TestEndBlocker(t *testing.T) {
t.Parallel()

Expand Down
4 changes: 3 additions & 1 deletion gno.land/pkg/gnoland/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,9 @@ type TxWithMetadata struct {
}

type GnoTxMetadata struct {
Timestamp int64 `json:"timestamp"`
Timestamp int64 `json:"timestamp"`
BlockHeight int64 `json:"block_height,omitempty"`
ChainID string `json:"chain_id,omitempty"`
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you planning to use the ChainID field in a future PR? As it stands, it looks like dead code.

I see it’s being populated on the tx-archive side, but I don’t really get the goal there either (looks like it's never read). Maybe your intent is to specify where a TX comes from in each jsonl line? If that's the goal, I’m wondering if just defining it at the file level would be enough, so we don't unnecessarily duplicate the same data on every line of the jsonl.

Either way, it’d be good to add a comment explaining why this field is there.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The goal is to ensure the exported value is complete for validating signatures and replaying under similar conditions. We aim to use it as soon as it makes sense, but we want to make it available now to ensure completeness.

}

// ReadGenesisTxs reads the genesis txs from the given file path
Expand Down
Loading