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

Pull Genesis parsing out of state #2284

Closed
wants to merge 38 commits into from
Closed

Conversation

abi87
Copy link
Contributor

@abi87 abi87 commented Nov 9, 2023

Why this should be merged

Building setup for unit test in platformVm is cumbersome. Once of the reasons is that we use genesisBytes to initialize the platformVM state. This forces us to build genesis and serialize it in UTs.
This PR drop the bytes and pass a genesisState data structure, thus simplifying the UTs context. This PR kills 130 LOC without giving up any functionality.
Thanks @joshua-kim and @marun for the discussions that brought me to these changes.

How this works

Let state.State accept a genesis state instead of its bytes, moving the unmarshalling to the client

How this was tested

CI

@abi87 abi87 self-assigned this Nov 9, 2023
@abi87 abi87 requested a review from dhrubabasu November 9, 2023 18:24
vm.state, err = state.New(
vm.db,
genesisBytes,
g,
Copy link
Contributor Author

@abi87 abi87 Nov 9, 2023

Choose a reason for hiding this comment

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

by pulling the byte parsing out of state and passing a well defined object instead, we open the way to simplify state testing. State is surprisingly not well tests (with a few notable exceptions), and part of the reason is the complex building of genesis state. This PR solves this problem

Comment on lines -61 to -101
avaxAssetID := ids.GenerateTestID()
genesisTime := time.Now().Truncate(time.Second)
genesisEndTime := genesisTime.Add(28 * 24 * time.Hour)

addr, err := address.FormatBech32(constants.UnitTestHRP, ids.GenerateTestShortID().Bytes())
require.NoError(err)

genesisValidators := []api.GenesisPermissionlessValidator{{
GenesisValidator: api.GenesisValidator{
StartTime: json.Uint64(genesisTime.Unix()),
EndTime: json.Uint64(genesisEndTime.Unix()),
NodeID: ids.GenerateTestNodeID(),
},
RewardOwner: &api.Owner{
Threshold: 1,
Addresses: []string{addr},
},
Staked: []api.UTXO{{
Amount: json.Uint64(2 * units.KiloAvax),
Address: addr,
}},
DelegationFee: reward.PercentDenominator,
}}

buildGenesisArgs := api.BuildGenesisArgs{
NetworkID: json.Uint32(constants.UnitTestID),
AvaxAssetID: avaxAssetID,
UTXOs: nil,
Validators: genesisValidators,
Chains: nil,
Time: json.Uint64(genesisTime.Unix()),
InitialSupply: json.Uint64(360 * units.MegaAvax),
Encoding: formatting.Hex,
}

buildGenesisResponse := api.BuildGenesisReply{}
platformvmSS := api.StaticService{}
require.NoError(platformvmSS.BuildGenesis(nil, &buildGenesisArgs, &buildGenesisResponse))

genesisBytes, err := formatting.Decode(buildGenesisResponse.Encoding, buildGenesisResponse.Bytes)
require.NoError(err)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

just moved to buildGenesisTest + dropped the formatting nonsense.

func buildGenesisTest(ctx *snow.Context) []byte {
genesisUTXOs := make([]api.UTXO, len(preFundedKeys))
func buildGenesisTest(ctx *snow.Context) *genesis.Genesis {
genesisUtxos := make([]*genesis.UTXO, len(preFundedKeys))
Copy link
Contributor Author

Choose a reason for hiding this comment

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

these lines are basically what platformvmSS.BuildGenesis used to do. Pulled up here and in other helpers_test.go files. In a following PR a drop the code duplication and centralize genesis creation in UTs

@@ -22,6 +24,8 @@ type Genesis struct {
Timestamp uint64 `serialize:"true"`
InitialSupply uint64 `serialize:"true"`
Message string `serialize:"true"`

GenesisID ids.ID `serialize:"false"`
Copy link
Contributor Author

Choose a reason for hiding this comment

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

we're changing a genesis related object but as long as changes are not serialized it's fine

Copy link
Contributor

Choose a reason for hiding this comment

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

Kinda want to see if we can remove this field, wrote a comment in state related to this

@@ -39,5 +43,7 @@ func Parse(genesisBytes []byte) (*Genesis, error) {
return nil, err
}
}

gen.GenesisID = hashing.ComputeHash256Array(genesisBytes)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

this is what we used to do in state package to derive genesisID. Just moved here

vms/platformvm/block/builder/helpers_test.go Outdated Show resolved Hide resolved
vms/platformvm/block/builder/helpers_test.go Show resolved Hide resolved
vms/platformvm/block/executor/helpers_test.go Show resolved Hide resolved
vms/platformvm/block/builder/helpers_test.go Show resolved Hide resolved
// Create the genesis block and save it as being accepted (We don't do
// genesisBlock.Accept() because then it'd look for genesisBlock's
// non-existent parent)
genesisID := hashing.ComputeHash256Array(genesisBytes)
genesisBlock, err := block.NewApricotCommitBlock(genesisID, 0 /*height*/)
genesisBlock, err := block.NewApricotCommitBlock(g.GenesisID, 0 /*height*/)
Copy link
Contributor

@joshua-kim joshua-kim Nov 21, 2023

Choose a reason for hiding this comment

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

Instead of having genesis id as a field attached to Genesis, I think it would be cleaner to just hash the genesis file here and have the caller pass in the id. This way we don't have to have the ID field in Genesis which is only ever used here anyways. I think the ID is more of a property of the genesis block than the genesis data itself

Copy link
Contributor Author

@abi87 abi87 Nov 22, 2023

Choose a reason for hiding this comment

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

@joshua-kim I don't really have a file to hash here, I have a byte slice. I understand your point about the ID being a block property.
I have replace the GenesisID attribute with GenesisBytes one and hashed it to get the genesis block ID wherever needed.
LMK if this is acceptable

EndTime: json.Uint64(defaultValidateEndTime.Unix()),
NodeID: nodeID,
vdrs := txheap.NewByEndTime()
for idx, nodeID := range genesisNodeIDs {
Copy link
Contributor

Choose a reason for hiding this comment

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

We initialize genesisNodeIDs in init(), so I think it would make sense for us to also initialize them here as well. Maybe we can move that blob of code down here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

But then what if we write a test that does not really use the environment? The genesisNodeIDs content would change depending on the order tests are executed right?
This is the reason I'd keep them in the init,

@@ -395,67 +394,81 @@ func defaultFx(clk *mockable.Clock, log logging.Logger, isBootstrapped bool) fx.
return res
}

func buildGenesisTest(ctx *snow.Context) []byte {
genesisUTXOs := make([]api.UTXO, len(preFundedKeys))
func buildGenesisTest(ctx *snow.Context) *genesis.Genesis {
Copy link
Contributor

@joshua-kim joshua-kim Nov 21, 2023

Choose a reason for hiding this comment

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

I actually didn't reailze we have a lot of genesis helpers to create pre-funded genesis. Wondering if it makes sense for us to consolidate this into a single place somewhere and export it out of a package. It could be something like:

package somewhere

func NewPrefundedGenesis(keys ...*secp2561.PrivateKey) (*TestGenesis, error) {
...
}

type *TestGenesis struct {
    *Genesis
    Keys []*secp256k1.PrivateKey
}

I'm not sure what package i'd put his in. Maybe we can make a package like genesis/test or put this in platformvm somewhere.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is what I do in #2107 for genesis helpers and subsequent PRs for other test helpers.
Sorry we got the code duplication here, just trying to segment changes limit PRs scope

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Also historically, duplication arised when I repackaged platformvm but the way test parameters were set were slightly different across packaged. #2107 and subsequent PR remove duplicated code and fixes test to make them work with the default values and not ony with the specific values used in different packages.

@abi87 abi87 requested a review from joshua-kim November 22, 2023 14:09
@abi87 abi87 linked an issue Dec 9, 2023 that may be closed by this pull request
Copy link
Contributor

@dhrubabasu dhrubabasu left a comment

Choose a reason for hiding this comment

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

I believe our current model for VMs is they are initialized with genesis bytes: https://docs.avax.network/build/vm/create/golang-vm-complex#virtual-machine-methods

This feels like a step in the wrong direction. I think to simplify testing, we could instead have a defaultGenesisBytes as a UT constant.

@marun
Copy link
Contributor

marun commented Dec 18, 2023

Why does the configuration for testing have to mirror non-test scenarios?

It's not like the temporary networks we use for e2e testing mirror the deployment semantics for fuji or mainnet.

@dhrubabasu
Copy link
Contributor

Why does the configuration for testing have to mirror non-test scenarios?

This PR modifies non-test files (specifically, adjusting the state struct to take in a genesis struct instead of bytes)

@abi87
Copy link
Contributor Author

abi87 commented Dec 19, 2023

I believe our current model for VMs is they are initialized with genesis bytes: https://docs.avax.network/build/vm/create/golang-vm-complex#virtual-machine-methods

This feels like a step in the wrong direction. I think to simplify testing, we could instead have a defaultGenesisBytes as a UT constant.

@dhrubabasu I am not changing the VM interface. Rather I am moving the byte parsing out of the state package.
I think right now the state package is under tested. One of the reasons is that is pretty curmbersome to create ad-hoc genesis content for tests. I want to write test genesis with no validators for instance, which are only useful in tests and while I can to it now, I have to go through the whole byte formattin shenanigans. Having a simpler struct where I directly specify the genesis content would make for much smaller tests.
Also defaultGenesisBytes as a UT constant would not offer enough flexibility for testing IMO

@dhrubabasu
Copy link
Contributor

dhrubabasu commented Dec 19, 2023

I think we can create a helper function that returns the genesisBytes. We shouldn't need to modify the state struct to create a test genesis with no validators. For example, this code works and uses the static service we use in defaultGenesis now. I feel abstracting this into a genesistest pkg helper would consolidate a lot of the code without modifying non-test code.

func main() {
	buildGenesisArgs := api.BuildGenesisArgs{
		Encoding:      formatting.Hex,
		NetworkID:     json.Uint32(constants.UnitTestID),
		AvaxAssetID:   ids.GenerateTestID(),
		UTXOs:         []api.UTXO{}, // No UTXOs
		Validators:    []api.GenesisPermissionlessValidator{}, // No Validators
		Chains:        nil,
		Time:          json.Uint64(time.Date(1997, 1, 1, 0, 0, 0, 0, time.UTC).Unix()),
		InitialSupply: json.Uint64(360 * units.MegaAvax),
	}

	buildGenesisResponse := api.BuildGenesisReply{}
	platformvmSS := api.StaticService{}
	err := platformvmSS.BuildGenesis(nil, &buildGenesisArgs, &buildGenesisResponse)
	if err != nil {
		panic(err)
	}

	genesisBytes, err := formatting.Decode(buildGenesisResponse.Encoding, buildGenesisResponse.Bytes)
	if err != nil {
		panic(err)
	}

	fmt.Println(genesisBytes)
}
[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 50 201 169 0 4 254 250 23 183 36 0 0 0 0]

@abi87 abi87 changed the title P-chain UTs consolidation 0 - Pulled Genesis parsing out of state Pull Genesis parsing out of state Jan 2, 2024
Copy link

github-actions bot commented Mar 3, 2024

This PR has become stale because it has been open for 30 days with no activity. Adding the lifecycle/frozen label will cause this PR to ignore lifecycle events.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Archived in project
Development

Successfully merging this pull request may close these issues.

Make P-chain UTs maintanable
5 participants