Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: add support for signing lazily loaded transactions in genesis #3468

Merged
merged 11 commits into from
Jan 9, 2025
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 }}
sw360cab marked this conversation as resolved.
Show resolved Hide resolved
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 (
// 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 (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 @@
"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 @@
fs.StringVar(
&c.genesisTxsFile,
"genesis-txs-file",
defaultGenesisTxsFile,
"",
"initial txs to replay",
)

Expand Down Expand Up @@ -218,7 +222,7 @@
)

// 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 @@
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 @@
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 @@
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 @@

gen.Validators = []bft.GenesisValidator{
{
Address: pk.Address(),
PubKey: pk,
Address: pubKey.Address(),
PubKey: pubKey,
Power: 10,
Name: "testvalidator",
},
Expand All @@ -398,22 +425,43 @@

// 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)
}

Check warning on line 440 in gno.land/cmd/gnoland/start.go

View check run for this annotation

Codecov / codecov/patch

gno.land/cmd/gnoland/start.go#L437-L440

Added lines #L437 - L440 were not covered by tests
}

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)
}

Check warning on line 448 in gno.land/cmd/gnoland/start.go

View check run for this annotation

Codecov / codecov/patch

gno.land/cmd/gnoland/start.go#L447-L448

Added lines #L447 - L448 were not covered by tests

// 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 @@
"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 @@

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)
}

Check warning on line 101 in gno.land/pkg/gnoland/types.go

View check run for this annotation

Codecov / codecov/patch

gno.land/pkg/gnoland/types.go#L100-L101

Added lines #L100 - L101 were not covered by tests

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

Check warning on line 106 in gno.land/pkg/gnoland/types.go

View check run for this annotation

Codecov / codecov/patch

gno.land/pkg/gnoland/types.go#L105-L106

Added lines #L105 - L106 were not covered by tests

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
Loading