Validate genesis model

This commit is contained in:
Alex Peters
2020-06-30 11:08:45 +02:00
parent 8aad0c9630
commit a20e568bff
9 changed files with 392 additions and 71 deletions

View File

@@ -25,12 +25,11 @@ func InitGenesis(ctx sdk.Context, keeper Keeper, data types.GenesisState) {
}
for _, contract := range data.Contracts {
keeper.setContractInfo(ctx, contract.ContractAddress, &contract.ContractInfo)
keeper.setContractState(ctx, contract.ContractAddress, contract.ContractState)
keeper.importContract(ctx, contract.ContractAddress, &contract.ContractInfo, contract.ContractState)
}
for _, seq := range data.Sequences {
keeper.setAutoIncrementID(ctx, seq.IDKey, seq.Value)
keeper.importAutoIncrementID(ctx, seq.IDKey, seq.Value)
}
}

View File

@@ -1,6 +1,7 @@
package keeper
import (
"crypto/sha256"
"io/ioutil"
"os"
"testing"
@@ -42,7 +43,7 @@ func TestGenesisExportImport(t *testing.T) {
contract.CodeID = codeID
contractAddr := srcKeeper.generateContractAddress(srcCtx, codeID)
srcKeeper.setContractInfo(srcCtx, contractAddr, &contract)
srcKeeper.setContractState(srcCtx, contractAddr, stateModels)
srcKeeper.importContractState(srcCtx, contractAddr, stateModels)
}
// export
@@ -67,6 +68,166 @@ func TestGenesisExportImport(t *testing.T) {
require.False(t, dstIT.Valid())
}
func TestFailFastImport(t *testing.T) {
wasmCode, err := ioutil.ReadFile("./testdata/contract.wasm")
require.NoError(t, err)
codeHash := sha256.Sum256(wasmCode)
anyAddress := make([]byte, 20)
specs := map[string]struct {
src types.GenesisState
expSuccess bool
}{
"happy path: code info correct": {
src: types.GenesisState{
Codes: []types.Code{{
CodeInfo: wasmTypes.CodeInfo{
CodeHash: codeHash[:],
Creator: anyAddress,
},
CodesBytes: wasmCode,
}},
Contracts: nil,
},
expSuccess: true,
},
"prevent code hash mismatch": {src: types.GenesisState{
Codes: []types.Code{{
CodeInfo: wasmTypes.CodeInfo{
CodeHash: make([]byte, len(codeHash)),
Creator: anyAddress,
},
CodesBytes: wasmCode,
}},
Contracts: nil,
}},
"happy path: code info and contract do match": {
src: types.GenesisState{
Codes: []types.Code{{
CodeInfo: wasmTypes.CodeInfo{
CodeHash: codeHash[:],
Creator: anyAddress,
},
CodesBytes: wasmCode,
}},
Contracts: []types.Contract{
{
ContractAddress: addrFromUint64(1<<32 + 1),
ContractInfo: wasmTypes.ContractInfo{
CodeID: 1,
Creator: anyAddress,
Label: "any",
Created: &types.AbsoluteTxPosition{BlockHeight: 1, TxIndex: 1},
},
},
},
},
expSuccess: true,
},
"prevent contracts that points to non existing codeID": {
src: types.GenesisState{
Contracts: []types.Contract{
{
ContractAddress: addrFromUint64(1<<32 + 1),
ContractInfo: wasmTypes.ContractInfo{
CodeID: 1,
Creator: anyAddress,
Label: "any",
Created: &types.AbsoluteTxPosition{BlockHeight: 1, TxIndex: 1},
},
},
},
},
},
"prevent duplicate contracts": {
src: types.GenesisState{
Codes: []types.Code{{
CodeInfo: wasmTypes.CodeInfo{
CodeHash: codeHash[:],
Creator: anyAddress,
},
CodesBytes: wasmCode,
}},
Contracts: []types.Contract{
{
ContractAddress: addrFromUint64(1<<32 + 1),
ContractInfo: wasmTypes.ContractInfo{
CodeID: 1,
Creator: anyAddress,
Label: "any",
Created: &types.AbsoluteTxPosition{BlockHeight: 1, TxIndex: 1},
},
}, {
ContractAddress: addrFromUint64(1<<32 + 1),
ContractInfo: wasmTypes.ContractInfo{
CodeID: 1,
Creator: anyAddress,
Label: "any",
Created: &types.AbsoluteTxPosition{BlockHeight: 1, TxIndex: 1},
},
},
},
},
},
"prevent duplicate contract model": {
src: types.GenesisState{
Codes: []types.Code{{
CodeInfo: wasmTypes.CodeInfo{
CodeHash: codeHash[:],
Creator: anyAddress,
},
CodesBytes: wasmCode,
}},
Contracts: []types.Contract{
{
ContractAddress: addrFromUint64(1<<32 + 1),
ContractInfo: wasmTypes.ContractInfo{
CodeID: 1,
Creator: anyAddress,
Label: "any",
Created: &types.AbsoluteTxPosition{BlockHeight: 1, TxIndex: 1},
},
ContractState: []types.Model{
{
Key: []byte{0x1},
Value: []byte("foo"),
},
{
Key: []byte{0x1},
Value: []byte("bar"),
},
},
},
},
},
},
"prevent duplicate sequences": {
src: types.GenesisState{
Sequences: []types.Sequence{
{IDKey: []byte("foo"), Value: 1},
{IDKey: []byte("foo"), Value: 9999},
},
},
},
}
for msg, spec := range specs {
t.Run(msg, func(t *testing.T) {
keeper, ctx, cleanup := setupKeeper(t)
defer cleanup()
require.NoError(t, types.ValidateGenesis(spec.src))
if spec.expSuccess {
InitGenesis(ctx, keeper, spec.src)
return
}
require.Panics(t, func() {
InitGenesis(ctx, keeper, spec.src)
})
})
}
}
func setupKeeper(t *testing.T) (Keeper, sdk.Context, func()) {
tempDir, err := ioutil.TempDir("", "wasm")
require.NoError(t, err)

View File

@@ -2,6 +2,7 @@ package keeper
import (
"encoding/binary"
"fmt"
"path/filepath"
"github.com/cosmos/cosmos-sdk/x/staking"
@@ -374,6 +375,11 @@ func (k Keeper) GetContractInfo(ctx sdk.Context, contractAddress sdk.AccAddress)
return &contract
}
func (k Keeper) containsContractInfo(ctx sdk.Context, contractAddress sdk.AccAddress) bool {
store := ctx.KVStore(k.storeKey)
return store.Has(types.GetContractAddressKey(contractAddress))
}
func (k Keeper) setContractInfo(ctx sdk.Context, contractAddress sdk.AccAddress, contract *types.ContractInfo) {
store := ctx.KVStore(k.storeKey)
store.Set(types.GetContractAddressKey(contractAddress), k.cdc.MustMarshalBinaryBare(contract))
@@ -398,13 +404,16 @@ func (k Keeper) GetContractState(ctx sdk.Context, contractAddress sdk.AccAddress
return prefixStore.Iterator(nil, nil)
}
func (k Keeper) setContractState(ctx sdk.Context, contractAddress sdk.AccAddress, models []types.Model) {
func (k Keeper) importContractState(ctx sdk.Context, contractAddress sdk.AccAddress, models []types.Model) {
prefixStoreKey := types.GetContractStorePrefixKey(contractAddress)
prefixStore := prefix.NewStore(ctx.KVStore(k.storeKey), prefixStoreKey)
for _, model := range models {
if model.Value == nil {
model.Value = []byte{}
}
if prefixStore.Has(model.Key) {
panic(fmt.Sprintf("duplicate key: %x", model.Key))
}
prefixStore.Set(model.Key, model.Value)
}
}
@@ -420,6 +429,11 @@ func (k Keeper) GetCodeInfo(ctx sdk.Context, codeID uint64) *types.CodeInfo {
return &codeInfo
}
func (k Keeper) containsCodeInfo(ctx sdk.Context, codeID uint64) bool {
store := ctx.KVStore(k.storeKey)
return store.Has(types.GetCodeKey(codeID))
}
func (k Keeper) GetByteCode(ctx sdk.Context, codeID uint64) ([]byte, error) {
store := ctx.KVStore(k.storeKey)
var codeInfo types.CodeInfo
@@ -496,12 +510,26 @@ func (k Keeper) peekAutoIncrementID(ctx sdk.Context, lastIDKey []byte) uint64 {
return id
}
func (k Keeper) setAutoIncrementID(ctx sdk.Context, lastIDKey []byte, val uint64) {
func (k Keeper) importAutoIncrementID(ctx sdk.Context, lastIDKey []byte, val uint64) {
store := ctx.KVStore(k.storeKey)
if store.Has(lastIDKey) {
panic(fmt.Sprintf("duplicate autoincrement id: %s", string(lastIDKey)))
}
bz := sdk.Uint64ToBigEndian(val)
store.Set(lastIDKey, bz)
}
func (k Keeper) importContract(ctx sdk.Context, address sdk.AccAddress, c *types.ContractInfo, state []types.Model) {
if !k.containsCodeInfo(ctx, c.CodeID) {
panic(fmt.Sprintf("unknown code id: %d", c.CodeID))
}
if k.containsContractInfo(ctx, address) {
panic(fmt.Sprintf("duplicate contract: %s", address))
}
k.setContractInfo(ctx, address, c)
k.importContractState(ctx, address, state)
}
func addrFromUint64(id uint64) sdk.AccAddress {
addr := make([]byte, 20)
addr[0] = 'C'

View File

@@ -48,7 +48,7 @@ func TestQueryContractState(t *testing.T) {
{Key: []byte("foo"), Value: []byte(`"bar"`)},
{Key: []byte{0x0, 0x1}, Value: []byte(`{"count":8}`)},
}
keeper.setContractState(ctx, addr, contractModel)
keeper.importContractState(ctx, addr, contractModel)
// this gets us full error, not redacted sdk.Error
q := NewQuerier(keeper)

View File

@@ -37,4 +37,13 @@ var (
// ErrMigrationFailed error for rust execution contract failure
ErrMigrationFailed = sdkErrors.Register(DefaultCodespace, 10, "migrate wasm contract failed")
// ErrEmpty error for empty content
ErrEmpty = sdkErrors.Register(DefaultCodespace, 11, "empty")
// ErrLimit error for content that exceeds a limit
ErrLimit = sdkErrors.Register(DefaultCodespace, 12, "exceeds limit")
// ErrInvalid error for content that is invalid in this context
ErrInvalid = sdkErrors.Register(DefaultCodespace, 13, "invalid")
)

View File

@@ -1,12 +1,22 @@
package types
import sdk "github.com/cosmos/cosmos-sdk/types"
import (
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
)
type Sequence struct {
IDKey []byte `json:"id_key"`
Value uint64 `json:"value"`
}
func (s Sequence) ValidateBasic() error {
if len(s.IDKey) == 0 {
return sdkerrors.Wrap(ErrEmpty, "id key")
}
return nil
}
// GenesisState is the struct representation of the export genesis
type GenesisState struct {
Codes []Code `json:"codes"`
@@ -14,12 +24,41 @@ type GenesisState struct {
Sequences []Sequence `json:"sequences"`
}
func (s GenesisState) ValidateBasic() error {
for i := range s.Codes {
if err := s.Codes[i].ValidateBasic(); err != nil {
return sdkerrors.Wrapf(err, "code: %d", i)
}
}
for i := range s.Contracts {
if err := s.Contracts[i].ValidateBasic(); err != nil {
return sdkerrors.Wrapf(err, "contract: %d", i)
}
}
for i := range s.Sequences {
if err := s.Sequences[i].ValidateBasic(); err != nil {
return sdkerrors.Wrapf(err, "sequence: %d", i)
}
}
return nil
}
// Code struct encompasses CodeInfo and CodeBytes
type Code struct {
CodeInfo CodeInfo `json:"code_info"`
CodesBytes []byte `json:"code_bytes"`
}
func (c Code) ValidateBasic() error {
if err := c.CodeInfo.ValidateBasic(); err != nil {
return sdkerrors.Wrap(err, "code info")
}
if err := validateWasmCode(c.CodesBytes); err != nil {
return sdkerrors.Wrap(err, "code bytes")
}
return nil
}
// Contract struct encompasses ContractAddress, ContractInfo, and ContractState
type Contract struct {
ContractAddress sdk.AccAddress `json:"contract_address"`
@@ -27,8 +66,23 @@ type Contract struct {
ContractState []Model `json:"contract_state"`
}
func (c Contract) ValidateBasic() error {
if err := sdk.VerifyAddressFormat(c.ContractAddress); err != nil {
return sdkerrors.Wrap(err, "contract address")
}
if err := c.ContractInfo.ValidateBasic(); err != nil {
return sdkerrors.Wrap(err, "contract info")
}
for i := range c.ContractState {
if err := c.ContractState[i].ValidateBasic(); err != nil {
return sdkerrors.Wrapf(err, "contract state %d", i)
}
}
return nil
}
// ValidateGenesis performs basic validation of supply genesis data returning an
// error for any failed validation criteria.
func ValidateGenesis(data GenesisState) error {
return nil
return data.ValidateBasic()
}

View File

@@ -2,35 +2,11 @@ package types
import (
"encoding/json"
"net/url"
"regexp"
sdk "github.com/cosmos/cosmos-sdk/types"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
)
const (
MaxWasmSize = 500 * 1024
// MaxLabelSize is the longest label that can be used when Instantiating a contract
MaxLabelSize = 128
// BuildTagRegexp is a docker image regexp.
// We only support max 128 characters, with at least one organization name (subset of all legal names).
//
// Details from https://docs.docker.com/engine/reference/commandline/tag/#extended-description :
//
// An image name is made up of slash-separated name components (optionally prefixed by a registry hostname).
// Name components may contain lowercase characters, digits and separators.
// A separator is defined as a period, one or two underscores, or one or more dashes. A name component may not start or end with a separator.
//
// A tag name must be valid ASCII and may contain lowercase and uppercase letters, digits, underscores, periods and dashes.
// A tag name may not start with a period or a dash and may contain a maximum of 128 characters.
BuildTagRegexp = "^[a-z0-9][a-z0-9._-]*[a-z0-9](/[a-z0-9][a-z0-9._-]*[a-z0-9])+:[a-zA-Z0-9_][a-zA-Z0-9_.-]*$"
MaxBuildTagSize = 128
)
type MsgStoreCode struct {
Sender sdk.AccAddress `json:"sender" yaml:"sender"`
// WASMByteCode can be raw or gzip compressed
@@ -54,28 +30,18 @@ func (msg MsgStoreCode) ValidateBasic() error {
return err
}
if len(msg.WASMByteCode) == 0 {
return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "empty wasm code")
if err := validateWasmCode(msg.WASMByteCode); err != nil {
return sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, "code bytes %s", err.Error())
}
if len(msg.WASMByteCode) > MaxWasmSize {
return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "wasm code too large")
if err := validateSourceURL(msg.Source); err != nil {
return sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, "source %s", err.Error())
}
if msg.Source != "" {
u, err := url.Parse(msg.Source)
if err != nil {
return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "source should be a valid url")
}
if !u.IsAbs() {
return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "source should be an absolute url")
}
if u.Scheme != "https" {
return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "source must use https")
}
if err := validateBuilder(msg.Builder); err != nil {
return sdkerrors.Wrapf(sdkerrors.ErrInvalidRequest, "builder %s", err.Error())
}
return validateBuilder(msg.Builder)
return nil
}
func (msg MsgStoreCode) GetSignBytes() []byte {
@@ -86,21 +52,6 @@ func (msg MsgStoreCode) GetSigners() []sdk.AccAddress {
return []sdk.AccAddress{msg.Sender}
}
func validateBuilder(buildTag string) error {
if len(buildTag) > MaxBuildTagSize {
return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "builder tag longer than 128 characters")
}
if buildTag != "" {
ok, err := regexp.MatchString(BuildTagRegexp, buildTag)
if err != nil || !ok {
return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "invalid tag supplied for builder")
}
}
return nil
}
type MsgInstantiateContract struct {
Sender sdk.AccAddress `json:"sender" yaml:"sender"`
// Admin is an optional address that can execute migrations
@@ -127,11 +78,9 @@ func (msg MsgInstantiateContract) ValidateBasic() error {
if msg.Code == 0 {
return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "code_id is required")
}
if msg.Label == "" {
return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "label is required")
}
if len(msg.Label) > MaxLabelSize {
return sdkerrors.Wrap(sdkerrors.ErrInvalidRequest, "label cannot be longer than 128 characters")
if err := validateLabel(msg.Label); err != nil {
return err
}
if msg.InitFunds.IsAnyNegative() {
@@ -143,7 +92,6 @@ func (msg MsgInstantiateContract) ValidateBasic() error {
return err
}
}
return nil
}

View File

@@ -3,6 +3,7 @@ package types
import (
"encoding/json"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
tmBytes "github.com/tendermint/tendermint/libs/bytes"
wasmTypes "github.com/CosmWasm/go-cosmwasm/types"
@@ -20,6 +21,13 @@ type Model struct {
Value []byte `json:"val"`
}
func (m Model) ValidateBasic() error {
if len(m.Key) == 0 {
return sdkerrors.Wrap(ErrEmpty, "key")
}
return nil
}
// CodeInfo is data for the uploaded contract WASM code
type CodeInfo struct {
CodeHash []byte `json:"code_hash"`
@@ -28,6 +36,22 @@ type CodeInfo struct {
Builder string `json:"builder"`
}
func (c CodeInfo) ValidateBasic() error {
if len(c.CodeHash) == 0 {
return sdkerrors.Wrap(ErrEmpty, "code hash")
}
if err := sdk.VerifyAddressFormat(c.Creator); err != nil {
return sdkerrors.Wrap(err, "creator")
}
if err := validateSourceURL(c.Source); err != nil {
return sdkerrors.Wrap(err, "source")
}
if err := validateBuilder(c.Builder); err != nil {
return sdkerrors.Wrap(err, "builder")
}
return nil
}
// NewCodeInfo fills a new Contract struct
func NewCodeInfo(codeHash []byte, creator sdk.AccAddress, source string, builder string) CodeInfo {
return CodeInfo{
@@ -58,6 +82,24 @@ func (c *ContractInfo) UpdateCodeID(ctx sdk.Context, newCodeID uint64) {
c.LastUpdated = NewCreatedAt(ctx)
}
func (c *ContractInfo) ValidateBasic() error {
if c.CodeID == 0 {
return sdkerrors.Wrap(ErrEmpty, "code id")
}
if err := sdk.VerifyAddressFormat(c.Creator); err != nil {
return sdkerrors.Wrap(err, "creator")
}
if c.Admin != nil {
if err := sdk.VerifyAddressFormat(c.Admin); err != nil {
return sdkerrors.Wrap(err, "admin")
}
}
if err := validateLabel(c.Label); err != nil {
return sdkerrors.Wrap(err, "label")
}
return nil
}
// AbsoluteTxPosition can be used to sort contracts
type AbsoluteTxPosition struct {
// BlockHeight is the block the contract was created at

View File

@@ -0,0 +1,80 @@
package types
import (
"net/url"
"regexp"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
)
const (
MaxWasmSize = 500 * 1024
// MaxLabelSize is the longest label that can be used when Instantiating a contract
MaxLabelSize = 128
// BuildTagRegexp is a docker image regexp.
// We only support max 128 characters, with at least one organization name (subset of all legal names).
//
// Details from https://docs.docker.com/engine/reference/commandline/tag/#extended-description :
//
// An image name is made up of slash-separated name components (optionally prefixed by a registry hostname).
// Name components may contain lowercase characters, digits and separators.
// A separator is defined as a period, one or two underscores, or one or more dashes. A name component may not start or end with a separator.
//
// A tag name must be valid ASCII and may contain lowercase and uppercase letters, digits, underscores, periods and dashes.
// A tag name may not start with a period or a dash and may contain a maximum of 128 characters.
BuildTagRegexp = "^[a-z0-9][a-z0-9._-]*[a-z0-9](/[a-z0-9][a-z0-9._-]*[a-z0-9])+:[a-zA-Z0-9_][a-zA-Z0-9_.-]*$"
MaxBuildTagSize = 128
)
func validateSourceURL(source string) error {
if source != "" {
u, err := url.Parse(source)
if err != nil {
return sdkerrors.Wrap(ErrInvalid, "not an url")
}
if !u.IsAbs() {
return sdkerrors.Wrap(ErrInvalid, "not an absolute url")
}
if u.Scheme != "https" {
return sdkerrors.Wrap(ErrInvalid, "must use https")
}
}
return nil
}
func validateBuilder(buildTag string) error {
if len(buildTag) > MaxBuildTagSize {
return sdkerrors.Wrap(ErrLimit, "longer than 128 characters")
}
if buildTag != "" {
ok, err := regexp.MatchString(BuildTagRegexp, buildTag)
if err != nil || !ok {
return ErrInvalid
}
}
return nil
}
func validateWasmCode(s []byte) error {
if len(s) == 0 {
return sdkerrors.Wrap(ErrEmpty, "is required")
}
if len(s) > MaxWasmSize {
return sdkerrors.Wrapf(ErrLimit, "cannot be longer than %d bytes", MaxWasmSize)
}
return nil
}
func validateLabel(label string) error {
if label == "" {
return sdkerrors.Wrap(ErrEmpty, "is required")
}
if len(label) > MaxLabelSize {
return sdkerrors.Wrap(ErrLimit, "cannot be longer than 128 characters")
}
return nil
}