Skip to content

Commit

Permalink
feat: support metadata for genesis txs (#2941)
Browse files Browse the repository at this point in the history
## Description

This PR introduces metadata support for genesis transactions (such as
timestamps), in the form of a new Gno genesis state that's easily
generate-able.

Shoutout to @clockworkgr for sanity checking the idea, and providing
insights that ultimately led to this PR materializing.

**BREAKING CHANGE**
The `GnoGenesisState` is now modified, and all functionality that
references it (ex. `gnogenesis`, `tx-archive`) will need to adapt.

### What we wanted to accomplish

The Portal Loop does not save "time" information upon restarting (from
block 0). This means that any transaction that resulted in a Realm /
Package calling `time.Now()` will get differing results when these same
transactions are "replayed" as part of the loop (in the genesis state).

We wanted to somehow preserve this timestamp information when the
transactions (from a previous loop), are executed as part of the genesis
building process.

For example:
- Portal Loop chain is on block 100
- tx A results in a call to `time.Now()`, which returns time T, and
saves it somewhere (Realm state)
- the Portal Loop restarts, and uses all the transactions it encountered
in the past iteration as genesis txs
- the genesis is generated by executing the transactions
- when tx A is re-executed (this time as part of the genesis block),
**it should return time T, as with the original execution context**

It is worth noting that this functionality is something we want in
`gnodev`, so simple helpers on the Realms to update the timestamps
wouldn't work.

### What this PR does

I've tried to follow a couple of principles when working on this PR:
- don't break backwards compatibility
- don't modify critical APIs such as the SDK ABCI, but preserve
existing, working, functionality in its original form
- don't add another layer to the complexity pancake
- don't implement a solution that looks (and works) like a hack

I'm not a huge fan of execution hooks, so the solution proposed in this
PR doesn't rely on any hook mechanism. Not going with the hook approach
also significantly decreases the complexity and preserves readability.

The base of this solution is enabling execution context modification,
with minimal / no API changes.
Having functionality like this, we can tailor the context during
critical segments such as genesis generation, and we're not just limited
to timestamps (which is the primary use-case).

We also introduce a new type of `AppState`, called
`MetadataGenesisState`, where metadata is associated with the
transactions. We hide the actual `AppState` implementation behind an
interface, so existing tools and flows don't break, and work as normal.

### What this PR doesn't do

There is more work to be done if this PR is merged:
- we need to add support to `tx-archive` for supporting exporting txs
with metadata. Should be straightforward to do
- the portal loop also needs to be restarted with this new "mode"
enabled
- we need to add support to existing `gnoland genesis` commands to
support the new `MetadataGenesisState`. It is also straightforward, but
definitely a bit of work
- if we want support for something like this in gnodev, the export /
import code of gnodev also needs to be modified to support the new
genesis state type (cc @gfanton)
	- #2943

Related PRs and issues:
- #2751
- #2744 

cc @moul @thehowl @jeronimoalbi @ilgooz 

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

- [x] Added new tests, or not needed, or not feasible
- [x] Provided an example (e.g. screenshot) to aid review or the PR is
self-explanatory
- [x] Updated the official documentation or not needed
- [x] No breaking changes were made, or a `BREAKING CHANGE: xxx` message
was included in the description
- [x] 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>

---------

Co-authored-by: Manfred Touron <94029+moul@users.noreply.github.com>
  • Loading branch information
zivkovicmilos and moul authored Nov 4, 2024
1 parent af05780 commit c776e32
Show file tree
Hide file tree
Showing 29 changed files with 581 additions and 173 deletions.
10 changes: 8 additions & 2 deletions contribs/gnodev/cmd/gnodev/setup_node.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ func setupDevNode(

if devCfg.txsFile != "" { // Load txs files
var err error
nodeConfig.InitialTxs, err = parseTxs(devCfg.txsFile)
nodeConfig.InitialTxs, err = gnoland.ReadGenesisTxs(ctx, devCfg.txsFile)
if err != nil {
return nil, fmt.Errorf("unable to load transactions: %w", err)
}
Expand All @@ -35,7 +35,13 @@ func setupDevNode(

// Override balances and txs
nodeConfig.BalancesList = state.Balances
nodeConfig.InitialTxs = state.Txs

stateTxs := state.Txs
nodeConfig.InitialTxs = make([]gnoland.TxWithMetadata, len(stateTxs))

for index, nodeTx := range stateTxs {
nodeConfig.InitialTxs[index] = nodeTx
}

logger.Info("genesis file loaded", "path", devCfg.genesisFile, "txs", len(nodeConfig.InitialTxs))
}
Expand Down
23 changes: 0 additions & 23 deletions contribs/gnodev/cmd/gnodev/txs.go

This file was deleted.

26 changes: 17 additions & 9 deletions contribs/gnodev/pkg/dev/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ type NodeConfig struct {
BalancesList []gnoland.Balance
PackagesPathList []PackagePath
Emitter emitter.Emitter
InitialTxs []std.Tx
InitialTxs []gnoland.TxWithMetadata
TMConfig *tmcfg.Config
SkipFailingGenesisTxs bool
NoReplay bool
Expand Down Expand Up @@ -84,7 +84,7 @@ type Node struct {
loadedPackages int

// state
initialState, state []std.Tx
initialState, state []gnoland.TxWithMetadata
currentStateIndex int
}

Expand Down Expand Up @@ -154,7 +154,7 @@ func (n *Node) GetRemoteAddress() string {

// GetBlockTransactions returns the transactions contained
// within the specified block, if any
func (n *Node) GetBlockTransactions(blockNum uint64) ([]std.Tx, error) {
func (n *Node) GetBlockTransactions(blockNum uint64) ([]gnoland.TxWithMetadata, error) {
n.muNode.RLock()
defer n.muNode.RUnlock()

Expand All @@ -163,21 +163,26 @@ func (n *Node) GetBlockTransactions(blockNum uint64) ([]std.Tx, error) {

// GetBlockTransactions returns the transactions contained
// within the specified block, if any
func (n *Node) getBlockTransactions(blockNum uint64) ([]std.Tx, error) {
func (n *Node) getBlockTransactions(blockNum uint64) ([]gnoland.TxWithMetadata, error) {
int64BlockNum := int64(blockNum)
b, err := n.client.Block(&int64BlockNum)
if err != nil {
return []std.Tx{}, fmt.Errorf("unable to load block at height %d: %w", blockNum, err) // nothing to see here
return []gnoland.TxWithMetadata{}, fmt.Errorf("unable to load block at height %d: %w", blockNum, err) // nothing to see here
}

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

txs[i] = tx
txs[i] = gnoland.TxWithMetadata{
Tx: tx,
Metadata: &gnoland.GnoTxMetadata{
Timestamp: b.BlockMeta.Header.Time.Unix(),
},
}
}

return txs, nil
Expand Down Expand Up @@ -347,11 +352,14 @@ func (n *Node) SendTransaction(tx *std.Tx) error {
return nil
}

func (n *Node) getBlockStoreState(ctx context.Context) ([]std.Tx, error) {
func (n *Node) getBlockStoreState(ctx context.Context) ([]gnoland.TxWithMetadata, error) {
// get current genesis state
genesis := n.GenesisDoc().AppState.(gnoland.GnoGenesisState)

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

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

lastBlock := n.getLatestBlockNumber()
var blocnum uint64 = 1
for ; blocnum <= lastBlock; blocnum++ {
Expand Down
5 changes: 2 additions & 3 deletions contribs/gnodev/pkg/dev/node_state.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"github.com/gnolang/gno/contribs/gnodev/pkg/events"
"github.com/gnolang/gno/gno.land/pkg/gnoland"
bft "github.com/gnolang/gno/tm2/pkg/bft/types"
"github.com/gnolang/gno/tm2/pkg/std"
)

var ErrEmptyState = errors.New("empty state")
Expand All @@ -29,7 +28,7 @@ func (n *Node) SaveCurrentState(ctx context.Context) error {
}

// Export the current state as list of txs
func (n *Node) ExportCurrentState(ctx context.Context) ([]std.Tx, error) {
func (n *Node) ExportCurrentState(ctx context.Context) ([]gnoland.TxWithMetadata, error) {
n.muNode.RLock()
defer n.muNode.RUnlock()

Expand All @@ -42,7 +41,7 @@ func (n *Node) ExportCurrentState(ctx context.Context) ([]std.Tx, error) {
return state[:n.currentStateIndex], nil
}

func (n *Node) getState(ctx context.Context) ([]std.Tx, error) {
func (n *Node) getState(ctx context.Context) ([]gnoland.TxWithMetadata, error) {
if n.state == nil {
var err error
n.state, err = n.getBlockStoreState(ctx)
Expand Down
24 changes: 14 additions & 10 deletions contribs/gnodev/pkg/dev/packages.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"path/filepath"

"github.com/gnolang/gno/contribs/gnodev/pkg/address"
"github.com/gnolang/gno/gno.land/pkg/gnoland"
vmm "github.com/gnolang/gno/gno.land/pkg/sdk/vm"
gno "github.com/gnolang/gno/gnovm/pkg/gnolang"
"github.com/gnolang/gno/gnovm/pkg/gnomod"
Expand Down Expand Up @@ -118,7 +119,7 @@ func (pm PackagesMap) toList() gnomod.PkgList {
return list
}

func (pm PackagesMap) Load(fee std.Fee) ([]std.Tx, error) {
func (pm PackagesMap) Load(fee std.Fee) ([]gnoland.TxWithMetadata, error) {
pkgs := pm.toList()

sorted, err := pkgs.Sort()
Expand All @@ -127,7 +128,8 @@ func (pm PackagesMap) Load(fee std.Fee) ([]std.Tx, error) {
}

nonDraft := sorted.GetNonDraftPkgs()
txs := []std.Tx{}
txs := make([]gnoland.TxWithMetadata, 0, len(nonDraft))

for _, modPkg := range nonDraft {
pkg := pm[modPkg.Dir]
if pkg.Creator.IsZero() {
Expand All @@ -141,18 +143,20 @@ func (pm PackagesMap) Load(fee std.Fee) ([]std.Tx, error) {
}

// Create transaction
tx := std.Tx{
Fee: fee,
Msgs: []std.Msg{
vmm.MsgAddPackage{
Creator: pkg.Creator,
Deposit: pkg.Deposit,
Package: memPkg,
tx := gnoland.TxWithMetadata{
Tx: std.Tx{
Fee: fee,
Msgs: []std.Msg{
vmm.MsgAddPackage{
Creator: pkg.Creator,
Deposit: pkg.Deposit,
Package: memPkg,
},
},
},
}

tx.Signatures = make([]std.Signature, len(tx.GetSigners()))
tx.Tx.Signatures = make([]std.Signature, len(tx.Tx.GetSigners()))
txs = append(txs, tx)
}

Expand Down
9 changes: 4 additions & 5 deletions contribs/gnogenesis/internal/txs/txs.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"github.com/gnolang/gno/gno.land/pkg/gnoland"
"github.com/gnolang/gno/tm2/pkg/bft/types"
"github.com/gnolang/gno/tm2/pkg/commands"
"github.com/gnolang/gno/tm2/pkg/std"
)

type txsCfg struct {
Expand Down Expand Up @@ -47,7 +46,7 @@ func (c *txsCfg) RegisterFlags(fs *flag.FlagSet) {
}

// appendGenesisTxs saves the given transactions to the genesis doc
func appendGenesisTxs(genesis *types.GenesisDoc, txs []std.Tx) error {
func appendGenesisTxs(genesis *types.GenesisDoc, txs []gnoland.TxWithMetadata) error {
// Initialize the app state if it's not present
if genesis.AppState == nil {
genesis.AppState = gnoland.GnoGenesisState{}
Expand Down Expand Up @@ -77,7 +76,7 @@ func appendGenesisTxs(genesis *types.GenesisDoc, txs []std.Tx) error {
}

// txStore is a wrapper for TM2 transactions
type txStore []std.Tx
type txStore []gnoland.TxWithMetadata

// leftMerge merges the two tx stores, with
// preference to the left
Expand All @@ -86,7 +85,7 @@ func (i *txStore) leftMerge(b txStore) error {
txHashMap := make(map[string]struct{}, len(*i))

for _, tx := range *i {
txHash, err := getTxHash(tx)
txHash, err := getTxHash(tx.Tx)
if err != nil {
return err
}
Expand All @@ -95,7 +94,7 @@ func (i *txStore) leftMerge(b txStore) error {
}

for _, tx := range b {
txHash, err := getTxHash(tx)
txHash, err := getTxHash(tx.Tx)
if err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion contribs/gnogenesis/internal/txs/txs_add_packages.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ func execTxsAddPackages(
return errInvalidPackageDir
}

parsedTxs := make([]std.Tx, 0)
parsedTxs := make([]gnoland.TxWithMetadata, 0)
for _, path := range args {
// Generate transactions from the packages (recursively)
txs, err := gnoland.LoadPackagesFromDir(path, genesisDeployAddress, genesisDeployFee)
Expand Down
4 changes: 2 additions & 2 deletions contribs/gnogenesis/internal/txs/txs_add_packages_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,9 +118,9 @@ func TestGenesis_Txs_Add_Packages(t *testing.T) {
state := updatedGenesis.AppState.(gnoland.GnoGenesisState)

require.Equal(t, 1, len(state.Txs))
require.Equal(t, 1, len(state.Txs[0].Msgs))
require.Equal(t, 1, len(state.Txs[0].Tx.Msgs))

msgAddPkg, ok := state.Txs[0].Msgs[0].(vmm.MsgAddPackage)
msgAddPkg, ok := state.Txs[0].Tx.Msgs[0].(vmm.MsgAddPackage)
require.True(t, ok)

assert.Equal(t, packagePath, msgAddPkg.Package.Path)
Expand Down
21 changes: 4 additions & 17 deletions contribs/gnogenesis/internal/txs/txs_add_sheet.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,13 @@ import (
"context"
"errors"
"fmt"
"os"

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

var (
errInvalidTxsFile = errors.New("unable to open transactions file")
errNoTxsFileSpecified = errors.New("no txs file specified")
)
var errNoTxsFileSpecified = errors.New("no txs file specified")

// newTxsAddSheetCmd creates the genesis txs add sheet subcommand
func newTxsAddSheetCmd(txsCfg *txsCfg, io commands.IO) *commands.Command {
Expand Down Expand Up @@ -49,22 +45,13 @@ func execTxsAddSheet(
return errNoTxsFileSpecified
}

parsedTxs := make([]std.Tx, 0)
parsedTxs := make([]gnoland.TxWithMetadata, 0)
for _, file := range args {
file, loadErr := os.Open(file)
if loadErr != nil {
return fmt.Errorf("%w, %w", errInvalidTxsFile, loadErr)
}

txs, err := std.ParseTxs(ctx, file)
txs, err := gnoland.ReadGenesisTxs(ctx, file)
if err != nil {
return fmt.Errorf("unable to parse file, %w", err)
}

if err = file.Close(); err != nil {
return fmt.Errorf("unable to gracefully close file, %w", err)
}

parsedTxs = append(parsedTxs, txs...)
}

Expand Down
33 changes: 17 additions & 16 deletions contribs/gnogenesis/internal/txs/txs_add_sheet_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,33 +21,35 @@ import (
)

// generateDummyTxs generates dummy transactions
func generateDummyTxs(t *testing.T, count int) []std.Tx {
func generateDummyTxs(t *testing.T, count int) []gnoland.TxWithMetadata {
t.Helper()

txs := make([]std.Tx, count)
txs := make([]gnoland.TxWithMetadata, count)

for i := 0; i < count; i++ {
txs[i] = std.Tx{
Msgs: []std.Msg{
bank.MsgSend{
FromAddress: crypto.Address{byte(i)},
ToAddress: crypto.Address{byte((i + 1) % count)},
Amount: std.NewCoins(std.NewCoin(ugnot.Denom, 1)),
txs[i] = gnoland.TxWithMetadata{
Tx: std.Tx{
Msgs: []std.Msg{
bank.MsgSend{
FromAddress: crypto.Address{byte(i)},
ToAddress: crypto.Address{byte((i + 1) % count)},
Amount: std.NewCoins(std.NewCoin(ugnot.Denom, 1)),
},
},
Fee: std.Fee{
GasWanted: 1,
GasFee: std.NewCoin(ugnot.Denom, 1000000),
},
Memo: fmt.Sprintf("tx %d", i),
},
Fee: std.Fee{
GasWanted: 1,
GasFee: std.NewCoin(ugnot.Denom, 1000000),
},
Memo: fmt.Sprintf("tx %d", i),
}
}

return txs
}

// encodeDummyTxs encodes the transactions into amino JSON
func encodeDummyTxs(t *testing.T, txs []std.Tx) []string {
func encodeDummyTxs(t *testing.T, txs []gnoland.TxWithMetadata) []string {
t.Helper()

encodedTxs := make([]string, 0, len(txs))
Expand Down Expand Up @@ -104,8 +106,7 @@ func TestGenesis_Txs_Add_Sheets(t *testing.T) {
}

// Run the command
cmdErr := cmd.ParseAndRun(context.Background(), args)
assert.ErrorContains(t, cmdErr, errInvalidTxsFile.Error())
assert.Error(t, cmd.ParseAndRun(context.Background(), args))
})

t.Run("no txs file", func(t *testing.T) {
Expand Down
5 changes: 2 additions & 3 deletions contribs/gnogenesis/internal/txs/txs_export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (
"github.com/gnolang/gno/gno.land/pkg/gnoland"
"github.com/gnolang/gno/tm2/pkg/amino"
"github.com/gnolang/gno/tm2/pkg/commands"
"github.com/gnolang/gno/tm2/pkg/std"
"github.com/gnolang/gno/tm2/pkg/testutils"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -117,9 +116,9 @@ func TestGenesis_Txs_Export(t *testing.T) {
// Validate the transactions were written down
scanner := bufio.NewScanner(outputFile)

outputTxs := make([]std.Tx, 0)
outputTxs := make([]gnoland.TxWithMetadata, 0)
for scanner.Scan() {
var tx std.Tx
var tx gnoland.TxWithMetadata

require.NoError(t, amino.UnmarshalJSON(scanner.Bytes(), &tx))

Expand Down
Loading

0 comments on commit c776e32

Please sign in to comment.