Store code history for contract

This commit is contained in:
Alex Peters 2020-07-16 13:14:23 +02:00
parent 774f6d7876
commit f8dbfd12f0
No known key found for this signature in database
GPG Key ID: BD28388D49EE708D
10 changed files with 161 additions and 79 deletions

View File

@ -43,7 +43,7 @@ var (
GetContractAddressKey = types.GetContractAddressKey
GetContractStorePrefixKey = types.GetContractStorePrefixKey
NewCodeInfo = types.NewCodeInfo
NewCreatedAt = types.NewCreatedAt
NewCreatedAt = types.NewAbsoluteTxPosition
NewContractInfo = types.NewContractInfo
NewEnv = types.NewEnv
NewWasmCoins = types.NewWasmCoins

View File

@ -2,6 +2,7 @@ package keeper
import (
"crypto/sha256"
"encoding/json"
"io/ioutil"
"math/rand"
"os"
@ -29,8 +30,8 @@ func TestGenesisExportImport(t *testing.T) {
require.NoError(t, err)
// store some test data
f := fuzz.New().Funcs(FuzzAddr, FuzzAbsoluteTxPosition, FuzzContractInfo, FuzzStateModel, FuzzAccessType, FuzzAccessConfig)
for i := 0; i < 25; i++ {
f := fuzz.New().Funcs(ModelFuzzers...)
for i := 0; i < 1; i++ {
var (
codeInfo types.CodeInfo
contract types.ContractInfo
@ -39,7 +40,6 @@ func TestGenesisExportImport(t *testing.T) {
f.Fuzz(&codeInfo)
f.Fuzz(&contract)
f.Fuzz(&stateModels)
codeID, err := srcKeeper.Create(srcCtx, codeInfo.Creator, wasmCode, codeInfo.Source, codeInfo.Builder, &codeInfo.InstantiateConfig)
require.NoError(t, err)
contract.CodeID = codeID
@ -52,24 +52,28 @@ func TestGenesisExportImport(t *testing.T) {
srcKeeper.setParams(srcCtx, wasmParams)
// export
genesisState := ExportGenesis(srcCtx, srcKeeper)
exportedState := ExportGenesis(srcCtx, srcKeeper)
// order should not matter
rand.Shuffle(len(genesisState.Codes), func(i, j int) {
genesisState.Codes[i], genesisState.Codes[j] = genesisState.Codes[j], genesisState.Codes[i]
rand.Shuffle(len(exportedState.Codes), func(i, j int) {
exportedState.Codes[i], exportedState.Codes[j] = exportedState.Codes[j], exportedState.Codes[i]
})
rand.Shuffle(len(genesisState.Contracts), func(i, j int) {
genesisState.Contracts[i], genesisState.Contracts[j] = genesisState.Contracts[j], genesisState.Contracts[i]
rand.Shuffle(len(exportedState.Contracts), func(i, j int) {
exportedState.Contracts[i], exportedState.Contracts[j] = exportedState.Contracts[j], exportedState.Contracts[i]
})
rand.Shuffle(len(genesisState.Sequences), func(i, j int) {
genesisState.Sequences[i], genesisState.Sequences[j] = genesisState.Sequences[j], genesisState.Sequences[i]
rand.Shuffle(len(exportedState.Sequences), func(i, j int) {
exportedState.Sequences[i], exportedState.Sequences[j] = exportedState.Sequences[j], exportedState.Sequences[i]
})
exportedGenesis, err := json.Marshal(exportedState)
require.NoError(t, err)
// re-import
dstKeeper, dstCtx, dstStoreKeys, dstCleanup := setupKeeper(t)
defer dstCleanup()
InitGenesis(dstCtx, dstKeeper, genesisState)
var importState wasmTypes.GenesisState
err = json.Unmarshal(exportedGenesis, &importState)
require.NoError(t, err)
InitGenesis(dstCtx, dstKeeper, importState)
// compare whole DB
for j := range srcStoreKeys {

View File

@ -240,7 +240,7 @@ func (k Keeper) instantiate(ctx sdk.Context, codeID uint64, creator, admin sdk.A
}
// persist instance
createdAt := types.NewCreatedAt(ctx)
createdAt := types.NewAbsoluteTxPosition(ctx)
instance := types.NewContractInfo(codeID, creator, admin, initMsg, label, createdAt)
store.Set(types.GetContractAddressKey(contractAddress), k.cdc.MustMarshalBinaryBare(instance))
@ -340,7 +340,7 @@ func (k Keeper) migrate(ctx sdk.Context, contractAddress sdk.AccAddress, caller
events := types.ParseEvents(res.Log, contractAddress)
ctx.EventManager().EmitEvents(events)
contractInfo.UpdateCodeID(ctx, newCodeID)
contractInfo.AddMigration(ctx, newCodeID, msg)
k.setContractInfo(ctx, contractAddress, contractInfo)
if err := k.dispatchMessages(ctx, contractAddress, res.Messages); err != nil {

View File

@ -306,15 +306,23 @@ func TestInstantiate(t *testing.T) {
require.Equal(t, "cosmos18vd8fpwxzck93qlwghaj6arh4p7c5n89uzcee5", addr.String())
gasAfter := ctx.GasMeter().GasConsumed()
require.Equal(t, uint64(0x1155d), gasAfter-gasBefore)
require.Equal(t, uint64(0x11740), gasAfter-gasBefore)
// ensure it is stored properly
info := keeper.GetContractInfo(ctx, addr)
require.NotNil(t, info)
assert.Equal(t, info.Creator, creator)
assert.Equal(t, info.CodeID, contractID)
assert.Equal(t, info.InitMsg, json.RawMessage(initMsgBz))
assert.Equal(t, info.Label, "demo contract 1")
exp := []types.ContractCodeHistoryEntry{{
Operation: types.InitContractCodeHistoryType,
CodeID: contractID,
Updated: types.NewAbsoluteTxPosition(ctx),
Msg: json.RawMessage(initMsgBz),
}}
assert.Equal(t, exp, info.ContractCodeHistory)
}
func TestInstantiateWithDeposit(t *testing.T) {
@ -527,7 +535,7 @@ func TestExecute(t *testing.T) {
// make sure gas is properly deducted from ctx
gasAfter := ctx.GasMeter().GasConsumed()
require.Equal(t, uint64(0x11c16), gasAfter-gasBefore)
require.Equal(t, uint64(0x11c2e), gasAfter-gasBefore)
// ensure bob now exists and got both payments released
bobAcct = accKeeper.GetAccount(ctx, bob)
@ -772,11 +780,11 @@ func TestMigrate(t *testing.T) {
wasmCode, err := ioutil.ReadFile("./testdata/contract.wasm")
require.NoError(t, err)
originalContractID, err := keeper.Create(ctx, creator, wasmCode, "", "", nil)
originalCodeID, err := keeper.Create(ctx, creator, wasmCode, "", "", nil)
require.NoError(t, err)
newContractID, err := keeper.Create(ctx, creator, wasmCode, "", "", nil)
newCodeID, err := keeper.Create(ctx, creator, wasmCode, "", "", nil)
require.NoError(t, err)
require.NotEqual(t, originalContractID, newContractID)
require.NotEqual(t, originalCodeID, newCodeID)
_, _, anyAddr := keyPubAddr()
_, _, newVerifierAddr := keyPubAddr()
@ -805,33 +813,33 @@ func TestMigrate(t *testing.T) {
"all good with same code id": {
admin: creator,
caller: creator,
codeID: originalContractID,
codeID: originalCodeID,
migrateMsg: migMsgBz,
expVerifier: newVerifierAddr,
},
"all good with different code id": {
admin: creator,
caller: creator,
codeID: newContractID,
codeID: newCodeID,
migrateMsg: migMsgBz,
expVerifier: newVerifierAddr,
},
"all good with admin set": {
admin: fred,
caller: fred,
codeID: newContractID,
codeID: newCodeID,
migrateMsg: migMsgBz,
expVerifier: newVerifierAddr,
},
"prevent migration when admin was not set on instantiate": {
caller: creator,
codeID: originalContractID,
codeID: originalCodeID,
expErr: sdkerrors.ErrUnauthorized,
},
"prevent migration when not sent by admin": {
caller: creator,
admin: fred,
codeID: originalContractID,
codeID: originalCodeID,
expErr: sdkerrors.ErrUnauthorized,
},
"fail with non existing code id": {
@ -844,20 +852,20 @@ func TestMigrate(t *testing.T) {
admin: creator,
caller: creator,
overrideContractAddr: anyAddr,
codeID: originalContractID,
codeID: originalCodeID,
expErr: sdkerrors.ErrInvalidRequest,
},
"fail in contract with invalid migrate msg": {
admin: creator,
caller: creator,
codeID: originalContractID,
codeID: originalCodeID,
migrateMsg: bytes.Repeat([]byte{0x1}, 7),
expErr: types.ErrMigrationFailed,
},
"fail in contract without migrate msg": {
admin: creator,
caller: creator,
codeID: originalContractID,
codeID: originalCodeID,
expErr: types.ErrMigrationFailed,
},
}
@ -865,7 +873,7 @@ func TestMigrate(t *testing.T) {
for msg, spec := range specs {
t.Run(msg, func(t *testing.T) {
ctx = ctx.WithBlockHeight(ctx.BlockHeight() + 1)
addr, err := keeper.Instantiate(ctx, originalContractID, creator, spec.admin, initMsgBz, "demo contract", nil)
addr, err := keeper.Instantiate(ctx, originalCodeID, creator, spec.admin, initMsgBz, "demo contract", nil)
require.NoError(t, err)
if spec.overrideContractAddr != nil {
addr = spec.overrideContractAddr
@ -877,8 +885,19 @@ func TestMigrate(t *testing.T) {
}
cInfo := keeper.GetContractInfo(ctx, addr)
assert.Equal(t, spec.codeID, cInfo.CodeID)
assert.Equal(t, originalContractID, cInfo.PreviousCodeID)
assert.Equal(t, types.NewCreatedAt(ctx), cInfo.LastUpdated)
expHistory := []types.ContractCodeHistoryEntry{{
Operation: types.InitContractCodeHistoryType,
CodeID: originalCodeID,
Updated: types.NewAbsoluteTxPosition(ctx),
Msg: initMsgBz,
}, {
Operation: types.MigrateContractCodeHistoryType,
CodeID: spec.codeID,
Updated: types.NewAbsoluteTxPosition(ctx),
Msg: spec.migrateMsg,
}}
assert.Equal(t, expHistory, cInfo.ContractCodeHistory)
m := keeper.QueryRaw(ctx, addr, []byte("config"))
require.Len(t, m, 1)

View File

@ -104,7 +104,14 @@ func TestInstantiateProposal(t *testing.T) {
assert.Equal(t, oneAddress, cInfo.Creator)
assert.Equal(t, otherAddress, cInfo.Admin)
assert.Equal(t, "testing", cInfo.Label)
assert.Equal(t, src.InitMsg, cInfo.InitMsg)
expHistory := []types.ContractCodeHistoryEntry{{
Operation: types.InitContractCodeHistoryType,
CodeID: src.Code,
Updated: types.NewAbsoluteTxPosition(ctx),
Msg: src.InitMsg,
}}
assert.Equal(t, expHistory, cInfo.ContractCodeHistory)
}
func TestMigrateProposal(t *testing.T) {
@ -169,9 +176,21 @@ func TestMigrateProposal(t *testing.T) {
cInfo := wasmKeeper.GetContractInfo(ctx, contractAddr)
require.NotNil(t, cInfo)
assert.Equal(t, uint64(2), cInfo.CodeID)
assert.Equal(t, uint64(1), cInfo.PreviousCodeID)
assert.Equal(t, anyAddress, cInfo.Admin)
assert.Equal(t, "testing", cInfo.Label)
expHistory := []types.ContractCodeHistoryEntry{{
// Operation: types.InitContractCodeHistoryType,
// CodeID: 1,
// Updated: types.NewAbsoluteTxPosition(ctx),
// Msg: initMsgBz,
//}, {
Operation: types.MigrateContractCodeHistoryType,
CodeID: src.Code,
Updated: types.NewAbsoluteTxPosition(ctx),
Msg: src.MigrateMsg,
}}
assert.Equal(t, expHistory, cInfo.ContractCodeHistory)
}
func TestAdminProposals(t *testing.T) {

View File

@ -69,11 +69,7 @@ func queryContractInfo(ctx sdk.Context, bech string, req abci.RequestQuery, keep
if info == nil {
return []byte("null"), nil
}
// redact the Created field (just used for sorting, not part of public API)
info.Created = nil
info.LastUpdated = nil
info.PreviousCodeID = 0
redact(info)
infoWithAddress := ContractInfoWithAddress{
Address: addr,
ContractInfo: info,
@ -85,6 +81,15 @@ func queryContractInfo(ctx sdk.Context, bech string, req abci.RequestQuery, keep
return bz, nil
}
// redact clears all fields not in the public api
func redact(info *types.ContractInfo) {
info.Created = nil
for i := range info.ContractCodeHistory {
info.ContractCodeHistory[i].Updated = nil
info.ContractCodeHistory[i].Msg = nil
}
}
func queryContractListByCode(ctx sdk.Context, codeIDstr string, req abci.RequestQuery, keeper Keeper) ([]byte, error) {
codeID, err := strconv.ParseUint(codeIDstr, 10, 64)
if err != nil {
@ -94,8 +99,6 @@ func queryContractListByCode(ctx sdk.Context, codeIDstr string, req abci.Request
var contracts []ContractInfoWithAddress
keeper.ListContractInfo(ctx, func(addr sdk.AccAddress, info types.ContractInfo) bool {
if info.CodeID == codeID {
// remove init message on list
info.InitMsg = nil
// and add the address
infoWithAddress := ContractInfoWithAddress{
Address: addr,
@ -112,7 +115,7 @@ func queryContractListByCode(ctx sdk.Context, codeIDstr string, req abci.Request
})
// and remove that info for the final json (yes, the json:"-" tag doesn't work)
for i := range contracts {
contracts[i].Created = nil
redact(contracts[i].ContractInfo)
}
bz, err := json.MarshalIndent(contracts, "", " ")

View File

@ -208,7 +208,10 @@ func TestListContractByCodeOrdering(t *testing.T) {
assert.Equal(t, fmt.Sprintf("contract %d", i), contract.Label)
assert.NotEmpty(t, contract.Address)
// ensure these are not shown
assert.Nil(t, contract.InitMsg)
assert.Nil(t, contract.Created)
for _, entry := range contract.ContractCodeHistory {
assert.Nil(t, entry.Updated)
assert.Nil(t, entry.Msg)
}
}
}

View File

@ -1,12 +1,16 @@
package keeper
import (
"encoding/json"
"github.com/CosmWasm/wasmd/x/wasm/internal/types"
sdk "github.com/cosmos/cosmos-sdk/types"
fuzz "github.com/google/gofuzz"
tmBytes "github.com/tendermint/tendermint/libs/bytes"
)
var ModelFuzzers = []interface{}{FuzzAddr, FuzzAbsoluteTxPosition, FuzzContractInfo, FuzzStateModel, FuzzAccessType, FuzzAccessConfig, FuzzContractCodeHistory}
func FuzzAddr(m *sdk.AccAddress, c fuzz.Continue) {
*m = make([]byte, 20)
c.Read(*m)
@ -18,16 +22,29 @@ func FuzzAbsoluteTxPosition(m *types.AbsoluteTxPosition, c fuzz.Continue) {
}
func FuzzContractInfo(m *types.ContractInfo, c fuzz.Continue) {
const maxSize = 1024
m.CodeID = c.RandUint64()
FuzzAddr(&m.Creator, c)
FuzzAddr(&m.Admin, c)
m.Label = c.RandString()
m.InitMsg = make([]byte, c.RandUint64()%maxSize)
c.Read(m.InitMsg)
c.Fuzz(&m.Created)
c.Fuzz(&m.LastUpdated)
m.PreviousCodeID = c.RandUint64()
historyElements := c.Int() % 128 // 128 should be enough for tests
m.ContractCodeHistory = make([]types.ContractCodeHistoryEntry, historyElements)
for i := range m.ContractCodeHistory {
c.Fuzz(&m.ContractCodeHistory[i])
}
}
func FuzzContractCodeHistory(m *types.ContractCodeHistoryEntry, c fuzz.Continue) {
const maxMsgSize = 128
m.CodeID = c.RandUint64()
msg := make([]byte, c.RandUint64()%maxMsgSize)
c.Read(msg)
var err error
if m.Msg, err = json.Marshal(msg); err != nil {
panic(err)
}
c.Fuzz(&m.Updated)
m.Operation = types.AllCodeHistoryTypes[c.Int()%len(types.AllCodeHistoryTypes)]
}
func FuzzStateModel(m *types.Model, c fuzz.Continue) {

View File

@ -2,6 +2,7 @@ package types
import (
"encoding/json"
"sort"
sdkerrors "github.com/cosmos/cosmos-sdk/types/errors"
tmBytes "github.com/tendermint/tendermint/libs/bytes"
@ -64,24 +65,46 @@ func NewCodeInfo(codeHash []byte, creator sdk.AccAddress, source string, builder
}
}
// ContractInfo stores a WASM contract instance
type ContractInfo struct {
CodeID uint64 `json:"code_id"`
Creator sdk.AccAddress `json:"creator"`
Admin sdk.AccAddress `json:"admin,omitempty"`
Label string `json:"label"`
InitMsg json.RawMessage `json:"init_msg,omitempty"`
// never show this in query results, just use for sorting
// (Note: when using json tag "-" amino refused to serialize it...)
Created *AbsoluteTxPosition `json:"created,omitempty"`
LastUpdated *AbsoluteTxPosition `json:"last_updated,omitempty"`
PreviousCodeID uint64 `json:"previous_code_id,omitempty"`
type ContractCodeHistoryOperationType string
const (
InitContractCodeHistoryType ContractCodeHistoryOperationType = "Init"
MigrateContractCodeHistoryType ContractCodeHistoryOperationType = "Migrate"
)
var AllCodeHistoryTypes = []ContractCodeHistoryOperationType{InitContractCodeHistoryType, MigrateContractCodeHistoryType}
type ContractCodeHistoryEntry struct {
Operation ContractCodeHistoryOperationType `json:"operation"`
CodeID uint64 `json:"code_id"`
Updated *AbsoluteTxPosition `json:"updated,omitempty"`
Msg json.RawMessage `json:"msg,omitempty"`
}
func (c *ContractInfo) UpdateCodeID(ctx sdk.Context, newCodeID uint64) {
c.PreviousCodeID = c.CodeID
c.CodeID = newCodeID
c.LastUpdated = NewCreatedAt(ctx)
// ContractInfo stores a WASM contract instance
type ContractInfo struct {
CodeID uint64 `json:"code_id"`
Creator sdk.AccAddress `json:"creator"`
Admin sdk.AccAddress `json:"admin,omitempty"`
Label string `json:"label"`
// never show this in query results, just use for sorting
// (Note: when using json tag "-" amino refused to serialize it...)
Created *AbsoluteTxPosition `json:"created,omitempty"`
ContractCodeHistory []ContractCodeHistoryEntry `json:"contract_code_history"`
}
func (c *ContractInfo) AddMigration(ctx sdk.Context, codeID uint64, msg []byte) {
h := ContractCodeHistoryEntry{
Operation: MigrateContractCodeHistoryType,
CodeID: codeID,
Updated: NewAbsoluteTxPosition(ctx),
Msg: msg,
}
c.ContractCodeHistory = append(c.ContractCodeHistory, h)
sort.Slice(c.ContractCodeHistory, func(i, j int) bool {
return c.ContractCodeHistory[i].Updated.LessThan(c.ContractCodeHistory[j].Updated)
})
c.CodeID = codeID
}
func (c *ContractInfo) ValidateBasic() error {
@ -105,9 +128,7 @@ func (c *ContractInfo) ValidateBasic() error {
if err := c.Created.ValidateBasic(); err != nil {
return sdkerrors.Wrap(err, "created")
}
if err := c.LastUpdated.ValidateBasic(); err != nil {
return sdkerrors.Wrap(err, "last updated")
}
// TODO: validate history
return nil
}
@ -140,8 +161,8 @@ func (a *AbsoluteTxPosition) ValidateBasic() error {
return nil
}
// NewCreatedAt gets a timestamp from the context
func NewCreatedAt(ctx sdk.Context) *AbsoluteTxPosition {
// NewAbsoluteTxPosition gets a timestamp from the context
func NewAbsoluteTxPosition(ctx sdk.Context) *AbsoluteTxPosition {
// we must safely handle nil gas meters
var index uint64
meter := ctx.BlockGasMeter()
@ -160,9 +181,14 @@ func NewContractInfo(codeID uint64, creator, admin sdk.AccAddress, initMsg []byt
CodeID: codeID,
Creator: creator,
Admin: admin,
InitMsg: initMsg,
Label: label,
Created: createdAt,
ContractCodeHistory: []ContractCodeHistoryEntry{{
Operation: InitContractCodeHistoryType,
CodeID: codeID,
Updated: createdAt,
Msg: initMsg,
}},
}
}

View File

@ -42,9 +42,6 @@ func TestContractInfoValidateBasic(t *testing.T) {
srcMutator: func(c *ContractInfo) { c.Label = strings.Repeat("a", MaxLabelSize+1) },
expError: true,
},
"init msg empty": {
srcMutator: func(c *ContractInfo) { c.InitMsg = nil },
},
"created nil": {
srcMutator: func(c *ContractInfo) { c.Created = nil },
expError: true,
@ -53,12 +50,6 @@ func TestContractInfoValidateBasic(t *testing.T) {
srcMutator: func(c *ContractInfo) { c.Created = &AbsoluteTxPosition{BlockHeight: -1} },
expError: true,
},
"last updated nil": {
srcMutator: func(c *ContractInfo) { c.LastUpdated = nil },
},
"previous code id empty": {
srcMutator: func(c *ContractInfo) { c.PreviousCodeID = 0 },
},
}
for msg, spec := range specs {
t.Run(msg, func(t *testing.T) {