Skip to content

Commit

Permalink
fix: add support for signing lazily loaded transactions in genesis (#…
Browse files Browse the repository at this point in the history
…3468)

## Description

This PR fixes an issue where genesis transactions added to
`genesis.json` through `--lazy` fail, since the signatures are missing.

It also introduces support for disabling genesis sig verification
altogether.
Why this was needed:
- Portal Loop transactions are signed with a valid account number and
sequence (not 0), and when they are replayed (they are shoved into a new
aggregated `genesis.json`), their signatures are also migrated. Upon
initializing the chain, this would cause the signature verification to
fail (the sig verification process for genesis txs expects account
number and sequence values of 0, but this is not the case)

@moul, the transaction signatures in
`gno.land/genesis/genesis_txs.jsonl` are invalid, and will always fail
when being verified

---------

Co-authored-by: Nathan Toups <[email protected]>
  • Loading branch information
2 people authored and albttx committed Jan 10, 2025
1 parent 899f292 commit 6817bc6
Show file tree
Hide file tree
Showing 19 changed files with 207 additions and 113 deletions.
1 change: 1 addition & 0 deletions .github/workflows/portal-loop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ jobs:
labels: ${{ steps.meta.outputs.labels }}

test-portal-loop-docker-compose:
if: ${{ false }}
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
Expand Down
1 change: 0 additions & 1 deletion contribs/gnogenesis/internal/txs/txs_add_packages.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import (

const (
defaultAccount_Name = "test1"
defaultAccount_Address = "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5"
defaultAccount_Seed = "source bonus chronic canvas draft south burst lottery vacant surface solve popular case indicate oppose farm nothing bullet exhibit title speed wink action roast"
defaultAccount_publicKey = "gpub1pgfj7ard9eg82cjtv4u4xetrwqer2dntxyfzxz3pq0skzdkmzu0r9h6gny6eg8c9dc303xrrudee6z4he4y7cs5rnjwmyf40yaj"
)
Expand Down
104 changes: 76 additions & 28 deletions gno.land/cmd/gnoland/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,22 +45,20 @@ var startGraphic = strings.ReplaceAll(`
/___/
`, "'", "`")

var (
// Keep in sync with contribs/gnogenesis/internal/txs/txs_add_packages.go
genesisDeployAddress = crypto.MustAddressFromString("g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5") // test1
genesisDeployFee = std.NewFee(50000, std.MustParseCoin(ugnot.ValueString(1000000)))
)
// Keep in sync with contribs/gnogenesis/internal/txs/txs_add_packages.go
var genesisDeployFee = std.NewFee(50000, std.MustParseCoin(ugnot.ValueString(1000000)))

type startCfg struct {
gnoRootDir string // TODO: remove as part of https://github.com/gnolang/gno/issues/1952
skipFailingGenesisTxs bool // TODO: remove as part of https://github.com/gnolang/gno/issues/1952
genesisBalancesFile string // TODO: remove as part of https://github.com/gnolang/gno/issues/1952
genesisTxsFile string // TODO: remove as part of https://github.com/gnolang/gno/issues/1952
genesisRemote string // TODO: remove as part of https://github.com/gnolang/gno/issues/1952
genesisFile string
chainID string
dataDir string
lazyInit bool
gnoRootDir string // TODO: remove as part of https://github.com/gnolang/gno/issues/1952
skipFailingGenesisTxs bool // TODO: remove as part of https://github.com/gnolang/gno/issues/1952
skipGenesisSigVerification bool // TODO: remove as part of https://github.com/gnolang/gno/issues/1952
genesisBalancesFile string // TODO: remove as part of https://github.com/gnolang/gno/issues/1952
genesisTxsFile string // TODO: remove as part of https://github.com/gnolang/gno/issues/1952
genesisRemote string // TODO: remove as part of https://github.com/gnolang/gno/issues/1952
genesisFile string
chainID string
dataDir string
lazyInit bool

logLevel string
logFormat string
Expand All @@ -86,7 +84,6 @@ func newStartCmd(io commands.IO) *commands.Command {
func (c *startCfg) RegisterFlags(fs *flag.FlagSet) {
gnoroot := gnoenv.RootDir()
defaultGenesisBalancesFile := filepath.Join(gnoroot, "gno.land", "genesis", "genesis_balances.txt")
defaultGenesisTxsFile := filepath.Join(gnoroot, "gno.land", "genesis", "genesis_txs.jsonl")

fs.BoolVar(
&c.skipFailingGenesisTxs,
Expand All @@ -95,6 +92,13 @@ func (c *startCfg) RegisterFlags(fs *flag.FlagSet) {
"don't panic when replaying invalid genesis txs",
)

fs.BoolVar(
&c.skipGenesisSigVerification,
"skip-genesis-sig-verification",
false,
"don't panic when replaying invalidly signed genesis txs",
)

fs.StringVar(
&c.genesisBalancesFile,
"genesis-balances-file",
Expand All @@ -105,7 +109,7 @@ func (c *startCfg) RegisterFlags(fs *flag.FlagSet) {
fs.StringVar(
&c.genesisTxsFile,
"genesis-txs-file",
defaultGenesisTxsFile,
"",
"initial txs to replay",
)

Expand Down Expand Up @@ -218,7 +222,7 @@ func execStart(ctx context.Context, c *startCfg, io commands.IO) error {
)

// Init a new genesis.json
if err := lazyInitGenesis(io, c, genesisPath, privateKey.GetPubKey()); err != nil {
if err := lazyInitGenesis(io, c, genesisPath, privateKey.Key.PrivKey); err != nil {
return fmt.Errorf("unable to initialize genesis.json, %w", err)
}
}
Expand All @@ -238,7 +242,16 @@ func execStart(ctx context.Context, c *startCfg, io commands.IO) error {
minGasPrices := cfg.Application.MinGasPrices

// Create application and node
cfg.LocalApp, err = gnoland.NewApp(nodeDir, c.skipFailingGenesisTxs, evsw, logger, minGasPrices)
cfg.LocalApp, err = gnoland.NewApp(
nodeDir,
gnoland.GenesisAppConfig{
SkipFailingTxs: c.skipFailingGenesisTxs,
SkipSigVerification: c.skipGenesisSigVerification,
},
evsw,
logger,
minGasPrices,
)
if err != nil {
return fmt.Errorf("unable to create the Gnoland app, %w", err)
}
Expand Down Expand Up @@ -334,15 +347,15 @@ func lazyInitGenesis(
io commands.IO,
c *startCfg,
genesisPath string,
publicKey crypto.PubKey,
privateKey crypto.PrivKey,
) error {
// Check if the genesis.json is present
if osm.FileExists(genesisPath) {
return nil
}

// Generate the new genesis.json file
if err := generateGenesisFile(genesisPath, publicKey, c); err != nil {
if err := generateGenesisFile(genesisPath, privateKey, c); err != nil {
return fmt.Errorf("unable to generate genesis file, %w", err)
}

Expand All @@ -367,7 +380,21 @@ func initializeLogger(io io.WriteCloser, logLevel, logFormat string) (*zap.Logge
return log.GetZapLoggerFn(format)(io, level), nil
}

func generateGenesisFile(genesisFile string, pk crypto.PubKey, c *startCfg) error {
func generateGenesisFile(genesisFile string, privKey crypto.PrivKey, c *startCfg) error {
var (
pubKey = privKey.PubKey()
// There is an active constraint for gno.land transactions:
//
// All transaction messages' (MsgSend, MsgAddPkg...) "author" field,
// specific to the message type ("creator", "sender"...), must match
// the signature address contained in the transaction itself.
// This means that if MsgSend is originating from address A,
// the owner of the private key for address A needs to sign the transaction
// containing the message. Every message in a transaction needs to
// originate from the same account that signed the transaction
txSender = pubKey.Address()
)

gen := &bft.GenesisDoc{}
gen.GenesisTime = time.Now()
gen.ChainID = c.chainID
Expand All @@ -383,8 +410,8 @@ func generateGenesisFile(genesisFile string, pk crypto.PubKey, c *startCfg) erro

gen.Validators = []bft.GenesisValidator{
{
Address: pk.Address(),
PubKey: pk,
Address: pubKey.Address(),
PubKey: pubKey,
Power: 10,
Name: "testvalidator",
},
Expand All @@ -398,22 +425,43 @@ func generateGenesisFile(genesisFile string, pk crypto.PubKey, c *startCfg) erro

// Load examples folder
examplesDir := filepath.Join(c.gnoRootDir, "examples")
pkgsTxs, err := gnoland.LoadPackagesFromDir(examplesDir, genesisDeployAddress, genesisDeployFee)
pkgsTxs, err := gnoland.LoadPackagesFromDir(examplesDir, txSender, genesisDeployFee)
if err != nil {
return fmt.Errorf("unable to load examples folder: %w", err)
}

// Load Genesis TXs
genesisTxs, err := gnoland.LoadGenesisTxsFile(c.genesisTxsFile, c.chainID, c.genesisRemote)
if err != nil {
return fmt.Errorf("unable to load genesis txs file: %w", err)
var genesisTxs []gnoland.TxWithMetadata

if c.genesisTxsFile != "" {
genesisTxs, err = gnoland.LoadGenesisTxsFile(c.genesisTxsFile, c.chainID, c.genesisRemote)
if err != nil {
return fmt.Errorf("unable to load genesis txs file: %w", err)
}
}

genesisTxs = append(pkgsTxs, genesisTxs...)

// Sign genesis transactions, with the default key (test1)
if err = gnoland.SignGenesisTxs(genesisTxs, privKey, c.chainID); err != nil {
return fmt.Errorf("unable to sign genesis txs: %w", err)
}

// Make sure the genesis transaction author has sufficient
// balance to cover transaction deployments in genesis.
//
// During the init-chainer process, the account that authors the
// genesis transactions needs to have a sufficient balance
// to cover outstanding transaction costs.
// Since the cost can't be estimated upfront at this point, the balance
// set is an arbitrary value based on a "best guess" basis.
// There should be a larger discussion if genesis transactions should consume gas, at all
deployerBalance := int64(len(genesisTxs)) * 10_000_000 // ~10 GNOT per tx
balances.Set(txSender, std.NewCoins(std.NewCoin("ugnot", deployerBalance)))

// Construct genesis AppState.
defaultGenState := gnoland.DefaultGenState()
defaultGenState.Balances = balances
defaultGenState.Balances = balances.List()
defaultGenState.Txs = genesisTxs
gen.AppState = defaultGenState

Expand Down
22 changes: 19 additions & 3 deletions gno.land/pkg/gnoland/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,10 +182,25 @@ func NewAppWithOptions(cfg *AppOptions) (abci.Application, error) {
return baseApp, nil
}

// GenesisAppConfig wraps the most important
// genesis params relating to the App
type GenesisAppConfig struct {
SkipFailingTxs bool // does not stop the chain from starting if any tx fails
SkipSigVerification bool // does not verify the transaction signatures in genesis
}

// NewTestGenesisAppConfig returns a testing genesis app config
func NewTestGenesisAppConfig() GenesisAppConfig {
return GenesisAppConfig{
SkipFailingTxs: true,
SkipSigVerification: true,
}
}

// NewApp creates the gno.land application.
func NewApp(
dataRootDir string,
skipFailingGenesisTxs bool,
genesisCfg GenesisAppConfig,
evsw events.EventSwitch,
logger *slog.Logger,
minGasPrices string,
Expand All @@ -199,9 +214,10 @@ func NewApp(
GenesisTxResultHandler: PanicOnFailingTxResultHandler,
StdlibDir: filepath.Join(gnoenv.RootDir(), "gnovm", "stdlibs"),
},
MinGasPrices: minGasPrices,
MinGasPrices: minGasPrices,
SkipGenesisVerification: genesisCfg.SkipSigVerification,
}
if skipFailingGenesisTxs {
if genesisCfg.SkipFailingTxs {
cfg.GenesisTxResultHandler = NoopGenesisTxResultHandler
}

Expand Down
2 changes: 1 addition & 1 deletion gno.land/pkg/gnoland/app_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ func TestNewApp(t *testing.T) {
// NewApp should have good defaults and manage to run InitChain.
td := t.TempDir()

app, err := NewApp(td, true, events.NewEventSwitch(), log.NewNoopLogger(), "")
app, err := NewApp(td, NewTestGenesisAppConfig(), events.NewEventSwitch(), log.NewNoopLogger(), "")
require.NoError(t, err, "NewApp should be successful")

resp := app.InitChain(abci.RequestInitChain{
Expand Down
9 changes: 3 additions & 6 deletions gno.land/pkg/gnoland/genesis.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,15 @@ import (
const initGasPrice = "1ugnot/1000gas"

// LoadGenesisBalancesFile loads genesis balances from the provided file path.
func LoadGenesisBalancesFile(path string) ([]Balance, error) {
func LoadGenesisBalancesFile(path string) (Balances, error) {
// each balance is in the form: g1xxxxxxxxxxxxxxxx=100000ugnot
content, err := osm.ReadFile(path)
if err != nil {
return nil, err
}
lines := strings.Split(string(content), "\n")

balances := make([]Balance, 0, len(lines))
balances := make(Balances, len(lines))
for _, line := range lines {
line = strings.TrimSpace(line)

Expand Down Expand Up @@ -56,10 +56,7 @@ func LoadGenesisBalancesFile(path string) ([]Balance, error) {
return nil, fmt.Errorf("invalid balance coins %s: %w", parts[1], err)
}

balances = append(balances, Balance{
Address: addr,
Amount: coins,
})
balances.Set(addr, coins)
}

return balances, nil
Expand Down
29 changes: 29 additions & 0 deletions gno.land/pkg/gnoland/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"os"

"github.com/gnolang/gno/tm2/pkg/amino"
"github.com/gnolang/gno/tm2/pkg/crypto"
"github.com/gnolang/gno/tm2/pkg/sdk/auth"
"github.com/gnolang/gno/tm2/pkg/std"
)
Expand Down Expand Up @@ -86,3 +87,31 @@ func ReadGenesisTxs(ctx context.Context, path string) ([]TxWithMetadata, error)

return txs, nil
}

// SignGenesisTxs will sign all txs passed as argument using the private key.
// This signature is only valid for genesis transactions as the account number and sequence are 0
func SignGenesisTxs(txs []TxWithMetadata, privKey crypto.PrivKey, chainID string) error {
for index, tx := range txs {
// Upon verifying genesis transactions, the account number and sequence are considered to be 0.
// The reason for this is that it is not possible to know the account number (or sequence!) in advance
// when generating the genesis transaction signature
bytes, err := tx.Tx.GetSignBytes(chainID, 0, 0)
if err != nil {
return fmt.Errorf("unable to get sign bytes for transaction, %w", err)
}

signature, err := privKey.Sign(bytes)
if err != nil {
return fmt.Errorf("unable to sign genesis transaction, %w", err)
}

txs[index].Tx.Signatures = []std.Signature{
{
PubKey: privKey.PubKey(),
Signature: signature,
},
}
}

return nil
}
27 changes: 27 additions & 0 deletions gno.land/pkg/gnoland/types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/gnolang/gno/gno.land/pkg/gnoland/ugnot"
"github.com/gnolang/gno/tm2/pkg/amino"
"github.com/gnolang/gno/tm2/pkg/crypto"
"github.com/gnolang/gno/tm2/pkg/crypto/secp256k1"
"github.com/gnolang/gno/tm2/pkg/sdk/bank"
"github.com/gnolang/gno/tm2/pkg/std"
"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -129,3 +130,29 @@ func TestReadGenesisTxs(t *testing.T) {
}
})
}

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

var (
txs = generateTxs(t, 100)
privKey = secp256k1.GenPrivKey()
pubKey = privKey.PubKey()
chainID = "testing"
)

// Make sure the transactions are properly signed
require.NoError(t, SignGenesisTxs(txs, privKey, chainID))

// Make sure the signatures are valid
for _, tx := range txs {
payload, err := tx.Tx.GetSignBytes(chainID, 0, 0)
require.NoError(t, err)

sigs := tx.Tx.GetSignatures()
require.Len(t, sigs, 1)

assert.True(t, pubKey.Equals(sigs[0].PubKey))
assert.True(t, pubKey.VerifyBytes(payload, sigs[0].Signature))
}
}
2 changes: 1 addition & 1 deletion gno.land/pkg/integration/node_testing.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ func LoadDefaultGenesisBalanceFile(t TestingTS, gnoroot string) []gnoland.Balanc
genesisBalances, err := gnoland.LoadGenesisBalancesFile(balanceFile)
require.NoError(t, err)

return genesisBalances
return genesisBalances.List()
}

// LoadDefaultGenesisParamFile loads the default genesis balance file for testing.
Expand Down
3 changes: 1 addition & 2 deletions gno.land/pkg/integration/pkgloader.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,7 @@ func (pl *PkgsLoader) LoadPackages(creatorKey crypto.PrivKey, fee std.Fee, depos
}
}

err = SignTxs(txs, creatorKey, "tendermint_test")
if err != nil {
if err = gnoland.SignGenesisTxs(txs, creatorKey, "tendermint_test"); err != nil {
return nil, fmt.Errorf("unable to sign txs: %w", err)
}

Expand Down
Loading

0 comments on commit 6817bc6

Please sign in to comment.