From a3940c4965cb9a04f53042800267729079072a6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matev=C5=BE=20Jekovec?= Date: Tue, 10 Dec 2019 14:01:56 +0100 Subject: [PATCH] roothash/api: replace Genesis.Blocks w/ RuntimeStates --- .changelog/2426.breaking.md | 7 + Cargo.lock | 1 + .../tendermint/apps/roothash/genesis.go | 38 +++--- .../tendermint/apps/roothash/roothash.go | 18 ++- go/genesis/tests/tester.go | 125 ++++++++++-------- go/oasis-net-runner/fixtures/default.go | 1 + go/oasis-node/cmd/debug/storage/export.go | 14 +- go/oasis-node/cmd/genesis/genesis.go | 27 ++-- go/oasis-node/cmd/registry/runtime/runtime.go | 3 + go/oasis-test-runner/oasis/fixture.go | 2 + go/oasis-test-runner/oasis/runtime.go | 2 + .../scenario/e2e/halt_restore.go | 1 + .../scenario/e2e/registry_cli.go | 1 + go/registry/api/api.go | 5 +- go/registry/api/runtime.go | 42 +++++- go/registry/tests/tester.go | 4 +- go/roothash/api/api.go | 42 +----- keymanager-runtime/api/Cargo.toml | 1 + keymanager-runtime/api/src/api.rs | 1 + keymanager-runtime/api/src/lib.rs | 1 + runtime/src/common/bytes.rs | 86 ++++++++++-- runtime/src/common/mod.rs | 1 + runtime/src/common/registry.rs | 31 +++++ 23 files changed, 299 insertions(+), 155 deletions(-) create mode 100644 .changelog/2426.breaking.md create mode 100644 runtime/src/common/registry.rs diff --git a/.changelog/2426.breaking.md b/.changelog/2426.breaking.md new file mode 100644 index 00000000000..40dd73e14e8 --- /dev/null +++ b/.changelog/2426.breaking.md @@ -0,0 +1,7 @@ +Refactoring of roothash genesis block for runtime. + +- `RuntimeGenesis.Round` field was added to the roothash block for the runtime +which can be set by `--runtime.genesis.round` flag. +- The `RuntimeGenesis.StorageReceipt` field was replaced by `StorageReceipts` list, +one for each storage node. +- Support for `base64` encoding/decoding of `Bytes` was added in rust. \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 91424dd1d85..fbfe4fdc78e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -711,6 +711,7 @@ dependencies = [ name = "oasis-core-keymanager-api" version = "0.3.0-alpha" dependencies = [ + "base64 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", "failure 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "oasis-core-runtime 0.3.0-alpha", diff --git a/go/consensus/tendermint/apps/roothash/genesis.go b/go/consensus/tendermint/apps/roothash/genesis.go index 6ec85d8704a..89a83b1ed1d 100644 --- a/go/consensus/tendermint/apps/roothash/genesis.go +++ b/go/consensus/tendermint/apps/roothash/genesis.go @@ -8,12 +8,13 @@ import ( "github.com/oasislabs/oasis-core/go/common/crypto/signature" "github.com/oasislabs/oasis-core/go/consensus/tendermint/abci" registryState "github.com/oasislabs/oasis-core/go/consensus/tendermint/apps/registry/state" - genesisApi "github.com/oasislabs/oasis-core/go/genesis/api" - roothash "github.com/oasislabs/oasis-core/go/roothash/api" - "github.com/oasislabs/oasis-core/go/roothash/api/block" + genesisAPI "github.com/oasislabs/oasis-core/go/genesis/api" + "github.com/oasislabs/oasis-core/go/registry/api" + roothashAPI "github.com/oasislabs/oasis-core/go/roothash/api" + storageAPI "github.com/oasislabs/oasis-core/go/storage/api" ) -func (app *rootHashApplication) InitChain(ctx *abci.Context, request types.RequestInitChain, doc *genesisApi.Document) error { +func (app *rootHashApplication) InitChain(ctx *abci.Context, request types.RequestInitChain, doc *genesisAPI.Document) error { st := doc.RootHash // The per-runtime roothash state is done primarily via DeliverTx, but @@ -35,26 +36,25 @@ func (app *rootHashApplication) InitChain(ctx *abci.Context, request types.Reque return nil } -func (rq *rootHashQuerier) Genesis(ctx context.Context) (*roothash.Genesis, error) { +func (rq *rootHashQuerier) Genesis(ctx context.Context) (*roothashAPI.Genesis, error) { runtimes := rq.state.Runtimes() - // Get per-runtime blocks. - blocks := make(map[signature.PublicKey]*block.Block) + // Get per-runtime states. + rtStates := make(map[signature.PublicKey]*api.RuntimeGenesis) for _, rt := range runtimes { - blk := *rt.CurrentBlock - // Header should be a normal header for genesis. - blk.Header.HeaderType = block.Normal - // There should be no previous hash. - blk.Header.PreviousHash.Empty() - // No messages. - blk.Header.Messages = nil - // No storage signatures. - blk.Header.StorageSignatures = []signature.Signature{} - blocks[rt.Runtime.ID] = &blk + rtState := api.RuntimeGenesis{ + StateRoot: rt.CurrentBlock.Header.StateRoot, + // State is always empty in Genesis regardless of StateRoot. + State: storageAPI.WriteLog{}, + StorageReceipts: []signature.Signature{}, + Round: rt.CurrentBlock.Header.Round, + } + + rtStates[rt.Runtime.ID] = &rtState } - genesis := &roothash.Genesis{ - Blocks: blocks, + genesis := &roothashAPI.Genesis{ + RuntimeStates: rtStates, } return genesis, nil } diff --git a/go/consensus/tendermint/apps/roothash/roothash.go b/go/consensus/tendermint/apps/roothash/roothash.go index c275b3163bc..58f39c63531 100644 --- a/go/consensus/tendermint/apps/roothash/roothash.go +++ b/go/consensus/tendermint/apps/roothash/roothash.go @@ -382,12 +382,18 @@ func (app *rootHashApplication) onNewRuntime(ctx *abci.Context, runtime *registr } // Create genesis block. - genesisBlock := genesis.Blocks[runtime.ID] - if genesisBlock == nil { - now := ctx.Now().Unix() - genesisBlock = block.NewGenesisBlock(runtime.ID, uint64(now)) - if !runtime.Genesis.StateRoot.IsEmpty() { - genesisBlock.Header.StateRoot = runtime.Genesis.StateRoot + now := ctx.Now().Unix() + genesisBlock := block.NewGenesisBlock(runtime.ID, uint64(now)) + // Fill the Header fields with Genesis runtime states, if this was called during InitChain(). + genesisBlock.Header.Round = runtime.Genesis.Round + genesisBlock.Header.StateRoot = runtime.Genesis.StateRoot + genesisBlock.Header.StorageSignatures = runtime.Genesis.StorageReceipts + if ctx.IsInitChain() { + genesisRts := genesis.RuntimeStates[runtime.ID] + if genesisRts != nil { + genesisBlock.Header.Round = genesisRts.Round + genesisBlock.Header.StateRoot = genesisRts.StateRoot + genesisBlock.Header.StorageSignatures = runtime.Genesis.StorageReceipts } } diff --git a/go/genesis/tests/tester.go b/go/genesis/tests/tester.go index 9b2e318dfa9..4691a4f0faf 100644 --- a/go/genesis/tests/tester.go +++ b/go/genesis/tests/tester.go @@ -22,10 +22,11 @@ import ( keymanager "github.com/oasislabs/oasis-core/go/keymanager/api" cmdFlags "github.com/oasislabs/oasis-core/go/oasis-node/cmd/common/flags" registry "github.com/oasislabs/oasis-core/go/registry/api" - "github.com/oasislabs/oasis-core/go/roothash/api/block" + roothashAPI "github.com/oasislabs/oasis-core/go/roothash/api" scheduler "github.com/oasislabs/oasis-core/go/scheduler/api" staking "github.com/oasislabs/oasis-core/go/staking/api" stakingTests "github.com/oasislabs/oasis-core/go/staking/tests/debug" + storage "github.com/oasislabs/oasis-core/go/storage/api" ) var testDoc = &genesis.Document{ @@ -111,6 +112,7 @@ func TestGenesisSanityCheck(t *testing.T) { // First, set up a few things we'll need in the tests below. signer := memorySigner.NewTestSigner("genesis sanity checks signer") + signer2 := memorySigner.NewTestSigner("another genesis sanity checks signer") validPK := signer.Public() invalidPK := hex2pk("c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac037a") @@ -119,6 +121,8 @@ func TestGenesisSanityCheck(t *testing.T) { var emptyHash hash.Hash emptyHash.Empty() + var nonEmptyHash hash.Hash + _ = nonEmptyHash.UnmarshalHex("1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef") // Note that this test entity has no nodes by design, those will be added // later by various tests. @@ -233,74 +237,81 @@ func TestGenesisSanityCheck(t *testing.T) { require.Error(d.SanityCheck(), "invalid keymanager ID should be rejected") // Test roothash genesis checks. - d = *testDoc - d.RootHash.Blocks = make(map[signature.PublicKey]*block.Block) - d.RootHash.Blocks[validPK] = &block.Block{ - Header: block.Header{ - HeaderType: 123, - }, + // First we define a helper function for calling the SanityCheck() on RuntimeStates. + rtsSanityCheck := func(g roothashAPI.Genesis, isGenesis bool) error { + for _, rts := range g.RuntimeStates { + if err = rts.SanityCheck(isGenesis); err != nil { + return err + } + } + return nil } - require.Error(d.SanityCheck(), "invalid block header should be rejected") d = *testDoc - d.RootHash.Blocks = make(map[signature.PublicKey]*block.Block) - d.RootHash.Blocks[validPK] = &block.Block{ - Header: block.Header{ - HeaderType: block.Normal, - PreviousHash: hash.Hash{}, - }, + d.RootHash.RuntimeStates = make(map[signature.PublicKey]*registry.RuntimeGenesis) + d.RootHash.RuntimeStates[validPK] = ®istry.RuntimeGenesis{ + StateRoot: nonEmptyHash, + // Empty list of storage receipts. + StorageReceipts: []signature.Signature{}, } - require.Error(d.SanityCheck(), "invalid previous hash should be rejected") + require.Error(rtsSanityCheck(d.RootHash, false), "empty StorageReceipts for StateRoot should be rejected") + require.NoError(rtsSanityCheck(d.RootHash, true), "empty StorageReceipts for StateRoot should be ignored, if isGenesis=true") d = *testDoc - d.RootHash.Blocks = make(map[signature.PublicKey]*block.Block) - d.RootHash.Blocks[validPK] = &block.Block{ - Header: block.Header{ - HeaderType: block.Normal, - PreviousHash: emptyHash, - Timestamp: uint64(time.Now().Unix() + 62*60), - }, + d.RootHash.RuntimeStates = make(map[signature.PublicKey]*registry.RuntimeGenesis) + d.RootHash.RuntimeStates[validPK] = ®istry.RuntimeGenesis{ + StateRoot: nonEmptyHash, + // List with one empty (invalid) storage receipt. + StorageReceipts: []signature.Signature{signature.Signature{}}, } - require.Error(d.SanityCheck(), "invalid timestamp should be rejected") - - d = *testDoc - sigCtx := signature.NewContext("genesis sanity check storage sig test") - sig, grr := signature.Sign(signer, sigCtx, []byte{1, 2, 3}) - require.NoError(grr, "should be able to sign") - d.RootHash.Blocks = make(map[signature.PublicKey]*block.Block) - d.RootHash.Blocks[validPK] = &block.Block{ - Header: block.Header{ - HeaderType: block.Normal, - PreviousHash: emptyHash, - Timestamp: uint64(time.Now().Unix()), - StorageSignatures: []signature.Signature{*sig}, - }, + require.Error(rtsSanityCheck(d.RootHash, false), "empty StorageReceipt for StateRoot should be rejected") + require.NoError(rtsSanityCheck(d.RootHash, true), "empty StorageReceipt for StateRoot should be ignored, if isGenesis=true") + + d = *testDoc + signature.SetChainContext("test: oasis-core tests") + stateRootSig, _ := signature.Sign(signer, storage.ReceiptSignatureContext, nonEmptyHash[:]) + stateRootSig2, _ := signature.Sign(signer2, storage.ReceiptSignatureContext, nonEmptyHash[:]) + wrongSig, _ := signature.Sign(signer, storage.ReceiptSignatureContext, []byte{1, 2, 3}) + d.RootHash.RuntimeStates = make(map[signature.PublicKey]*registry.RuntimeGenesis) + d.RootHash.RuntimeStates[validPK] = ®istry.RuntimeGenesis{ + StateRoot: nonEmptyHash, + // Some non-empty signature, but not related to StateRoot. + StorageReceipts: []signature.Signature{*wrongSig, *stateRootSig, *stateRootSig2}, } - require.Error(d.SanityCheck(), "non-empty storage signature array should be rejected") + require.Error(rtsSanityCheck(d.RootHash, false), "some incorrect StorageReceipt for StateRoot should be rejected") + require.NoError(rtsSanityCheck(d.RootHash, true), "some incorrect StorageReceipt for StateRoot should be ignored, if isGenesis=true") d = *testDoc - d.RootHash.Blocks = make(map[signature.PublicKey]*block.Block) - d.RootHash.Blocks[validPK] = &block.Block{ - Header: block.Header{ - HeaderType: block.Normal, - PreviousHash: emptyHash, - Timestamp: uint64(time.Now().Unix()), - StorageSignatures: []signature.Signature{}, - Messages: []*block.Message{nil, nil, nil}, - }, + d.RootHash.RuntimeStates = make(map[signature.PublicKey]*registry.RuntimeGenesis) + d.RootHash.RuntimeStates[validPK] = ®istry.RuntimeGenesis{ + StateRoot: nonEmptyHash, + StorageReceipts: []signature.Signature{*stateRootSig, *stateRootSig2}, } - require.Error(d.SanityCheck(), "non-empty roothash message array should be rejected") - - d = *testDoc - d.RootHash.Blocks = make(map[signature.PublicKey]*block.Block) - d.RootHash.Blocks[validPK] = &block.Block{ - Header: block.Header{ - HeaderType: block.Normal, - PreviousHash: emptyHash, - Timestamp: uint64(time.Now().Unix()), - }, + require.NoError(rtsSanityCheck(d.RootHash, false), "non-empty StateRoot with all correct StorageReceipts should pass") + require.NoError(rtsSanityCheck(d.RootHash, true), "non-empty StateRoot with all correct StorageReceipts should pass, if isGenesis=true") + + d = *testDoc + nonEmptyState := storage.WriteLog{storage.LogEntry{ + Key: []byte{1, 2, 3}, + Value: []byte{1, 2, 3}, + }} + d.RootHash.RuntimeStates = make(map[signature.PublicKey]*registry.RuntimeGenesis) + d.RootHash.RuntimeStates[validPK] = ®istry.RuntimeGenesis{ + State: nonEmptyState, + StateRoot: nonEmptyHash, + StorageReceipts: []signature.Signature{*wrongSig, *stateRootSig, *stateRootSig2}, + } + require.NoError(rtsSanityCheck(d.RootHash, false), "non-empty StateRoot with non-empty State and some invalid StorageReceipt should pass") + require.NoError(rtsSanityCheck(d.RootHash, true), "non-empty StateRoot with non-empty State and some invalid StorageReceipt should pass, if isGenesis=true") + + d.RootHash.RuntimeStates = make(map[signature.PublicKey]*registry.RuntimeGenesis) + d.RootHash.RuntimeStates[validPK] = ®istry.RuntimeGenesis{ + State: nonEmptyState, + StateRoot: nonEmptyHash, + StorageReceipts: []signature.Signature{*stateRootSig, *stateRootSig2}, } - require.NoError(d.SanityCheck(), "well-formed block should pass") + require.NoError(rtsSanityCheck(d.RootHash, false), "non-empty StateRoot with non-empty State and all valid StorageReceipts should pass") + require.NoError(rtsSanityCheck(d.RootHash, true), "non-empty StateRoot with non-empty State and all valid StorageReceipts should pass, if isGenesis=true") // Test registry genesis checks. d = *testDoc diff --git a/go/oasis-net-runner/fixtures/default.go b/go/oasis-net-runner/fixtures/default.go index 9a6c742c078..c7d0d7b4f49 100644 --- a/go/oasis-net-runner/fixtures/default.go +++ b/go/oasis-net-runner/fixtures/default.go @@ -98,6 +98,7 @@ func NewDefaultFixture() (*oasis.NetworkFixture, error) { }, Storage: registry.StorageParameters{GroupSize: 1}, GenesisState: viper.GetString(cfgRuntimeGenesisState), + GenesisRound: 0, }, }, Validators: []oasis.ValidatorFixture{ diff --git a/go/oasis-node/cmd/debug/storage/export.go b/go/oasis-node/cmd/debug/storage/export.go index 42a68928572..e3d341f2470 100644 --- a/go/oasis-node/cmd/debug/storage/export.go +++ b/go/oasis-node/cmd/debug/storage/export.go @@ -80,16 +80,20 @@ func doExport(cmd *cobra.Command, args []string) { defer storageBackend.Cleanup() // For each storage root. - for runtimeID, blk := range genesisDoc.RootHash.Blocks { + for runtimeID, rtg := range genesisDoc.RootHash.RuntimeStates { logger.Info("fetching checkpoint write log", "runtime_id", runtimeID, ) + // Use RuntimeID for the Roothash namespace. + var ns common.Namespace + _ = ns.UnmarshalBinary(runtimeID[:]) + // Get the checkpoint iterator. root := storageAPI.Root{ - Namespace: blk.Header.Namespace, - Round: blk.Header.Round, - Hash: blk.Header.StateRoot, + Namespace: ns, + Round: rtg.Round, + Hash: rtg.StateRoot, } it, err := storageBackend.GetCheckpoint(context.Background(), &storageAPI.GetCheckpointRequest{ @@ -108,7 +112,7 @@ func doExport(cmd *cobra.Command, args []string) { fn := fmt.Sprintf("storage-dump-%v-%d.json", runtimeID.String(), - blk.Header.Round, + rtg.Round, ) fn = filepath.Join(destDir, fn) if err = exportIterator(fn, &root, it); err != nil { diff --git a/go/oasis-node/cmd/genesis/genesis.go b/go/oasis-node/cmd/genesis/genesis.go index 6d78383d990..2209e41b9aa 100644 --- a/go/oasis-node/cmd/genesis/genesis.go +++ b/go/oasis-node/cmd/genesis/genesis.go @@ -33,7 +33,6 @@ import ( cmdGrpc "github.com/oasislabs/oasis-core/go/oasis-node/cmd/common/grpc" registry "github.com/oasislabs/oasis-core/go/registry/api" roothash "github.com/oasislabs/oasis-core/go/roothash/api" - "github.com/oasislabs/oasis-core/go/roothash/api/block" scheduler "github.com/oasislabs/oasis-core/go/scheduler/api" staking "github.com/oasislabs/oasis-core/go/staking/api" stakingTests "github.com/oasislabs/oasis-core/go/staking/tests/debug" @@ -406,39 +405,37 @@ func AppendRegistryState(doc *genesis.Document, entities, runtimes, nodes []stri // of exported roothash blocks. func AppendRootHashState(doc *genesis.Document, exports []string, l *logging.Logger) error { rootSt := roothash.Genesis{ - Blocks: make(map[signature.PublicKey]*block.Block), + RuntimeStates: make(map[signature.PublicKey]*registry.RuntimeGenesis), } for _, v := range exports { b, err := ioutil.ReadFile(v) if err != nil { - l.Error("failed to load genesis roothash blocks", + l.Error("failed to load genesis roothash runtime states", "err", err, "filename", v, ) return err } - var blocks []*block.Block - if err = json.Unmarshal(b, &blocks); err != nil { - l.Error("failed to parse genesis roothash blocks", + var rtStates map[signature.PublicKey]*registry.RuntimeGenesis + if err = json.Unmarshal(b, &rtStates); err != nil { + l.Error("failed to parse genesis roothash runtime states", "err", err, "filename", v, ) return err } - for _, blk := range blocks { - var key signature.PublicKey - copy(key[:], blk.Header.Namespace[:]) - if _, ok := rootSt.Blocks[key]; ok { - l.Error("duplicate genesis roothash block", - "runtime_id", blk.Header.Namespace, - "block", blk, + for key, rtg := range rtStates { + if _, ok := rootSt.RuntimeStates[key]; ok { + l.Error("duplicate genesis roothash runtime state", + "runtime_id", key, + "block", rtg, ) - return errors.New("duplicate genesis roothash block") + return errors.New("duplicate genesis roothash runtime states") } - rootSt.Blocks[key] = blk + rootSt.RuntimeStates[key] = rtg } } diff --git a/go/oasis-node/cmd/registry/runtime/runtime.go b/go/oasis-node/cmd/registry/runtime/runtime.go index 36f39c0f6b9..29417b79fe2 100644 --- a/go/oasis-node/cmd/registry/runtime/runtime.go +++ b/go/oasis-node/cmd/registry/runtime/runtime.go @@ -38,6 +38,7 @@ const ( CfgID = "runtime.id" CfgTEEHardware = "runtime.tee_hardware" CfgGenesisState = "runtime.genesis.state" + CfgGenesisRound = "runtime.genesis.round" CfgKind = "runtime.kind" CfgKeyManager = "runtime.keymanager" cfgOutput = "runtime.genesis.file" @@ -264,6 +265,7 @@ func runtimeFromFlags() (*registry.Runtime, signature.Signer, error) { // TODO: Support root upload when registering. gen := registry.RuntimeGenesis{} + gen.Round = viper.GetUint64(CfgGenesisRound) switch state := viper.GetString(CfgGenesisState); state { case "": gen.StateRoot.Empty() @@ -431,6 +433,7 @@ func init() { runtimeFlags.String(CfgID, "", "Runtime ID") runtimeFlags.String(CfgTEEHardware, "invalid", "Type of TEE hardware. Supported values are \"invalid\" and \"intel-sgx\"") runtimeFlags.String(CfgGenesisState, "", "Runtime state at genesis") + runtimeFlags.Uint64(CfgGenesisRound, 0, "Runtime round at genesis") runtimeFlags.String(CfgKeyManager, "", "Key Manager Runtime ID") runtimeFlags.String(CfgKind, "compute", "Kind of runtime. Supported values are \"compute\" and \"keymanager\"") runtimeFlags.String(CfgVersion, "", "Runtime version. Value is 64-bit hex e.g. 0x0000000100020003 for 1.2.3") diff --git a/go/oasis-test-runner/oasis/fixture.go b/go/oasis-test-runner/oasis/fixture.go index 5113029b3c3..f5e4bad4cad 100644 --- a/go/oasis-test-runner/oasis/fixture.go +++ b/go/oasis-test-runner/oasis/fixture.go @@ -157,6 +157,7 @@ type RuntimeFixture struct { Binary string `json:"binary"` GenesisState string `json:"genesis_state"` + GenesisRound uint64 `json:"genesis_round"` Compute registry.ComputeParameters `json:"compute"` Merge registry.MergeParameters `json:"merge"` @@ -198,6 +199,7 @@ func (f *RuntimeFixture) Create(netFixture *NetworkFixture, net *Network) (*Runt Storage: f.Storage, Binary: f.Binary, GenesisState: f.GenesisState, + GenesisRound: f.GenesisRound, Pruner: f.Pruner, }) } diff --git a/go/oasis-test-runner/oasis/runtime.go b/go/oasis-test-runner/oasis/runtime.go index 630fb80dad2..fc2076df53a 100644 --- a/go/oasis-test-runner/oasis/runtime.go +++ b/go/oasis-test-runner/oasis/runtime.go @@ -45,6 +45,7 @@ type RuntimeCfg struct { // nolint: maligned Binary string GenesisState string + GenesisRound uint64 Compute registry.ComputeParameters Merge registry.MergeParameters @@ -88,6 +89,7 @@ func (net *Network) NewRuntime(cfg *RuntimeCfg) (*Runtime, error) { "--" + common.CfgDataDir, rtDir.String(), "--" + cmdRegRt.CfgID, cfg.ID.String(), "--" + cmdRegRt.CfgKind, cfg.Kind.String(), + "--" + cmdRegRt.CfgGenesisRound, strconv.FormatUint(cfg.GenesisRound, 10), } if cfg.Kind == registry.KindCompute { args = append(args, []string{ diff --git a/go/oasis-test-runner/scenario/e2e/halt_restore.go b/go/oasis-test-runner/scenario/e2e/halt_restore.go index df5fab6752f..dc2ffd3972f 100644 --- a/go/oasis-test-runner/scenario/e2e/halt_restore.go +++ b/go/oasis-test-runner/scenario/e2e/halt_restore.go @@ -183,6 +183,7 @@ func (sc *haltRestoreImpl) Run(childEnv *env.Env) error { if err != nil { sc.logger.Error("scenario/e2e/halt_restore: failed getting genesis file provider", "err", err, + "genesis_file", files[0], ) return err } diff --git a/go/oasis-test-runner/scenario/e2e/registry_cli.go b/go/oasis-test-runner/scenario/e2e/registry_cli.go index b78d1167964..65503b3df2c 100644 --- a/go/oasis-test-runner/scenario/e2e/registry_cli.go +++ b/go/oasis-test-runner/scenario/e2e/registry_cli.go @@ -663,6 +663,7 @@ func (r *registryCLIImpl) genRegisterRuntimeTx(childEnv *env.Env, runtime regist "--" + cmdRegRt.CfgID, runtime.ID.String(), "--" + cmdRegRt.CfgTEEHardware, runtime.TEEHardware.String(), "--" + cmdRegRt.CfgGenesisState, genesisStateFile, + "--" + cmdRegRt.CfgGenesisRound, strconv.FormatUint(runtime.Genesis.Round, 10), "--" + cmdRegRt.CfgKind, runtime.Kind.String(), "--" + cmdRegRt.CfgVersion, runtime.Version.Version.String(), "--" + cmdRegRt.CfgVersionEnclave, string(runtime.Version.TEE), diff --git a/go/registry/api/api.go b/go/registry/api/api.go index 9bbd20c5101..b5a589b6e5d 100644 --- a/go/registry/api/api.go +++ b/go/registry/api/api.go @@ -889,9 +889,12 @@ func VerifyRegisterRuntimeArgs(logger *logging.Logger, sigRt *SignedRuntime, isG } if !isGenesis && !rt.Genesis.StateRoot.IsEmpty() { - // TODO: Verify storage receipt for the state root, reject such registrations for now. + // TODO: Verify storage receipt for the state root, reject such registrations for now. See oasis-core#1686. return nil, ErrInvalidArgument } + if err := rt.Genesis.SanityCheck(isGenesis); err != nil { + return nil, err + } // Ensure there is at least one member of the compute group. if rt.Compute.GroupSize == 0 { diff --git a/go/registry/api/runtime.go b/go/registry/api/runtime.go index 4e95002d6bc..45087492651 100644 --- a/go/registry/api/runtime.go +++ b/go/registry/api/runtime.go @@ -231,10 +231,44 @@ type RuntimeGenesis struct { StateRoot hash.Hash `json:"state_root"` // State is the state identified by the StateRoot. It may be empty iff - // the StorageReceipt is not invalid or StateRoot is an empty hash. + // all StorageReceipts are valid or StateRoot is an empty hash or if used + // in network genesis (e.g. during consensus chain init). State storage.WriteLog `json:"state"` - // StorageReceipt is the storage receipt for the state root. It may be - // invalid iff the State is non-empty or StateRoot is an empty hash. - StorageReceipt signature.Signature `json:"storage_receipt"` + // StorageReceipts are the storage receipts for the state root. The list + // may be empty or a signature in the list invalid iff the State is non- + // empty or StateRoot is an empty hash or if used in network genesis + // (e.g. during consensus chain init). + StorageReceipts []signature.Signature `json:"storage_receipts"` + + // Round is the runtime round in the genesis. + Round uint64 `json:"round"` +} + +// SanityCheck does basic sanity checking of RuntimeGenesis. +// isGenesis is true, if it is called during consensus chain init. +func (rtg *RuntimeGenesis) SanityCheck(isGenesis bool) error { + if isGenesis { + return nil + } + + // Require that either State is non-empty or Storage receipt being valid or StateRoot being non-empty. + if len(rtg.State) == 0 && !rtg.StateRoot.IsEmpty() { + // If State is empty and StateRoot is not, then all StorageReceipts must correctly verify StorageRoot. + if len(rtg.StorageReceipts) == 0 { + return fmt.Errorf("runtimegenesis: sanity check failed: when State is empty either StorageReceipts must be populated or StateRoot must be empty") + } + for _, sr := range rtg.StorageReceipts { + if !sr.PublicKey.IsValid() { + return fmt.Errorf("runtimegenesis: sanity check failed: when State is empty either all StorageReceipts must be valid or StateRoot must be empty (public_key %s)", sr.PublicKey) + } + + // TODO: Even if Verify below succeeds, runtime registration should still be rejected until oasis-core#1686 is solved! + if !sr.Verify(storage.ReceiptSignatureContext, rtg.StateRoot[:]) { + return fmt.Errorf("runtimegenesis: sanity check failed: StorageReceipt verification on StateRoot failed (public_key %s)", sr.PublicKey) + } + } + } + + return nil } diff --git a/go/registry/tests/tester.go b/go/registry/tests/tester.go index 8c9835e91d2..fff19a497a3 100644 --- a/go/registry/tests/tester.go +++ b/go/registry/tests/tester.go @@ -1066,10 +1066,10 @@ func NewTestRuntime(seed []byte, entity *TestEntity) (*TestRuntime, error) { }, Storage: api.StorageParameters{GroupSize: 3}, Genesis: api.RuntimeGenesis{ - StorageReceipt: signature.Signature{ + StorageReceipts: []signature.Signature{{ // We don't want an invalid public key so we pass something. PublicKey: rt.Signer.Public(), - }, + }}, }, } if entity != nil { diff --git a/go/roothash/api/api.go b/go/roothash/api/api.go index 9e85b9ad950..63218f1bc10 100644 --- a/go/roothash/api/api.go +++ b/go/roothash/api/api.go @@ -11,6 +11,7 @@ import ( "github.com/oasislabs/oasis-core/go/common/errors" "github.com/oasislabs/oasis-core/go/common/pubsub" "github.com/oasislabs/oasis-core/go/consensus/api/transaction" + "github.com/oasislabs/oasis-core/go/registry/api" "github.com/oasislabs/oasis-core/go/roothash/api/block" "github.com/oasislabs/oasis-core/go/roothash/api/commitment" ) @@ -153,8 +154,8 @@ type MetricsMonitorable interface { // Genesis is the roothash genesis state. type Genesis struct { - // Blocks is the per-runtime map of genesis blocks. - Blocks map[signature.PublicKey]*block.Block `json:"blocks,omitempty"` + // RuntimeStates is the per-runtime map of genesis blocks. + RuntimeStates map[signature.PublicKey]*api.RuntimeGenesis `json:"runtime_states,omitempty"` } // SanityCheckBlocks examines the blocks table. @@ -169,42 +170,13 @@ func SanityCheckBlocks(blocks map[signature.PublicKey]*block.Block) error { return nil } -// checkBlocksForGenesis examines the blocks for extra properties specific to genesis state. -func checkBlocksForGenesis(blocks map[signature.PublicKey]*block.Block) error { - for _, blk := range blocks { - hdr := blk.Header - - if hdr.HeaderType != block.Normal { - return fmt.Errorf("roothash: sanity check failed: invalid block header type %v", hdr.HeaderType) - } - - if !hdr.PreviousHash.IsEmpty() { - return fmt.Errorf("roothash: sanity check failed: non-empty previous hash") - } - - if len(hdr.StorageSignatures) != 0 { - return fmt.Errorf("roothash: sanity check failed: non-empty storage signatures") - } - - if len(hdr.Messages) != 0 { - return fmt.Errorf("roothash: sanity check failed: non-empty roothash messages") - } - } - return nil -} - // SanityCheck does basic sanity checking on the genesis state. func (g *Genesis) SanityCheck() error { // Check blocks. - err := SanityCheckBlocks(g.Blocks) - if err != nil { - return err - } - - err = checkBlocksForGenesis(g.Blocks) - if err != nil { - return err + for _, rtg := range g.RuntimeStates { + if err := rtg.SanityCheck(true); err != nil { + return err + } } - return nil } diff --git a/keymanager-runtime/api/Cargo.toml b/keymanager-runtime/api/Cargo.toml index 96d00ae55c1..b2fa29f40f4 100644 --- a/keymanager-runtime/api/Cargo.toml +++ b/keymanager-runtime/api/Cargo.toml @@ -4,6 +4,7 @@ version = "0.3.0-alpha" authors = ["Oasis Labs Inc. "] [dependencies] +base64 = "0.10.1" oasis-core-runtime = { path = "../../runtime" } serde = "1.0.71" serde_derive = "1.0" diff --git a/keymanager-runtime/api/src/api.rs b/keymanager-runtime/api/src/api.rs index 0aa41edc9e0..04d1cca9c94 100644 --- a/keymanager-runtime/api/src/api.rs +++ b/keymanager-runtime/api/src/api.rs @@ -1,5 +1,6 @@ use std::collections::HashMap; +use base64; use failure::Fail; use rand::{rngs::OsRng, Rng}; use serde_derive::{Deserialize, Serialize}; diff --git a/keymanager-runtime/api/src/lib.rs b/keymanager-runtime/api/src/lib.rs index b1f22a1ccfc..77c68c04190 100644 --- a/keymanager-runtime/api/src/lib.rs +++ b/keymanager-runtime/api/src/lib.rs @@ -1,4 +1,5 @@ //! Key manager API. +extern crate base64; extern crate failure; extern crate lazy_static; extern crate oasis_core_runtime; diff --git a/runtime/src/common/bytes.rs b/runtime/src/common/bytes.rs index deaecc9d479..d382023da2d 100644 --- a/runtime/src/common/bytes.rs +++ b/runtime/src/common/bytes.rs @@ -151,7 +151,11 @@ macro_rules! impl_bytes { where S: ::serde::Serializer, { - serializer.serialize_bytes(self.as_ref()) + if serializer.is_human_readable() { + serializer.serialize_str(&base64::encode(&self)) + } else { + serializer.serialize_bytes(self.as_ref()) + } } } @@ -171,37 +175,97 @@ macro_rules! impl_bytes { &self, formatter: &mut ::std::fmt::Formatter, ) -> ::std::fmt::Result { - formatter.write_str("bytes or sequence of u8") + formatter.write_str("bytes or string expected") } - fn visit_seq(self, mut seq: A) -> Result<$name, A::Error> + fn visit_str(self, data: &str) -> Result<$name, E> where - A: ::serde::de::SeqAccess<'de>, + E: ::serde::de::Error, { let mut array = [0; $size]; - for i in 0..$size { - array[i] = seq - .next_element()? - .ok_or_else(|| ::serde::de::Error::invalid_length(i, &self))?; + let bytes = match base64::decode(data) { + Ok(b) => b, + Err(err) => return match err { + base64::DecodeError::InvalidByte(pos, v) => Err(::serde::de::Error::custom(format!("invalid base64-encoded string: invalid byte '{}' at position {}", v, pos))), + base64::DecodeError::InvalidLength => Err(::serde::de::Error::custom(format!("invalid base64-encoded string: invalid length {}", data.len()))), + base64::DecodeError::InvalidLastSymbol(pos, v) => Err(::serde::de::Error::custom(format!("invalid base64-encoded string: invalid last symbol '{}' at position {}", v, pos))), + }, + }; + if bytes.len() != $size { + return Err(::serde::de::Error::invalid_length(bytes.len(), &self)); } + array[..].copy_from_slice(&bytes); + Ok($name(array)) } - fn visit_bytes(self, data: &[u8]) -> Result<$name, E> + fn visit_bytes(self, data: &[u8]) -> Result<$name, E> where E: ::serde::de::Error, { - let mut array = [0; $size]; if data.len() != $size { return Err(::serde::de::Error::invalid_length(data.len(), &self)); } + let mut array = [0; $size]; array[..].copy_from_slice(data); + Ok($name(array)) } } - Ok(deserializer.deserialize_bytes(BytesVisitor)?) + if deserializer.is_human_readable() { + Ok(deserializer.deserialize_string(BytesVisitor)?) + } else { + Ok(deserializer.deserialize_bytes(BytesVisitor)?) + } + } } }; } + +#[cfg(test)] +mod tests { + use super::*; + + // Use hash of an empty string as a test key. + const TEST_KEY_BYTES: [u8; 32] = [ + 0xc6, 0x72, 0xb8, 0xd1, 0xef, 0x56, 0xed, 0x28, 0xab, 0x87, 0xc3, 0x62, 0x2c, 0x51, 0x14, + 0x06, 0x9b, 0xdd, 0x3a, 0xd7, 0xb8, 0xf9, 0x73, 0x74, 0x98, 0xd0, 0xc0, 0x1e, 0xce, 0xf0, + 0x96, 0x7a, + ]; + + #[test] + fn test_serde_base64() { + // Serialize. + impl_bytes!(TestKey, 32, "test key"); + let test_key = TestKey(TEST_KEY_BYTES); + let test_key_str = serde_json::to_string(&test_key).unwrap(); + assert_eq!( + test_key_str, + "\"xnK40e9W7Sirh8NiLFEUBpvdOte4+XN0mNDAHs7wlno=\"" + ); + + // Deserialize. + let new_test_key: TestKey = serde_json::from_str(&test_key_str).unwrap(); + assert_eq!(new_test_key, test_key); + } + + #[test] + fn test_serde_cbor() { + // Serialize. + impl_bytes!(TestKey, 32, "test key"); + + let test_key = TestKey(TEST_KEY_BYTES); + let test_key_vec = serde_cbor::to_vec(&test_key).unwrap(); + + // CBOR prepends "X " to the binary value. + let mut expected_test_key_vec = vec![88, 32]; + expected_test_key_vec.extend_from_slice(&TEST_KEY_BYTES); + assert_eq!(test_key_vec, expected_test_key_vec); + + // Deserialize. + let new_test_key: TestKey = serde_cbor::from_slice(&test_key_vec).unwrap(); + assert_eq!(new_test_key, test_key); + } +} diff --git a/runtime/src/common/mod.rs b/runtime/src/common/mod.rs index 604e2c28a0d..bbb997512a4 100644 --- a/runtime/src/common/mod.rs +++ b/runtime/src/common/mod.rs @@ -6,6 +6,7 @@ pub mod cbor; pub mod crypto; pub mod key_format; pub mod logger; +pub mod registry; pub mod roothash; pub mod runtime; pub mod sgx; diff --git a/runtime/src/common/registry.rs b/runtime/src/common/registry.rs new file mode 100644 index 00000000000..949854e9e53 --- /dev/null +++ b/runtime/src/common/registry.rs @@ -0,0 +1,31 @@ +//! Registry structures. +//! +//! # Note +//! +//! This **MUST** be kept in sync with go/registry/api. +//! +use super::{ + super::storage::mkvs::WriteLog, + crypto::{hash, signature::SignatureBundle}, +}; +use serde_derive::{Deserialize, Serialize}; + +/// Runtime genesis information that is used to initialize runtime state in the first block. +#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct RuntimeGenesis { + /// State root that should be used at genesis time. If the runtime should start with empty state, + /// this must be set to the empty hash. + pub state_root: hash::Hash, + + /// State identified by the state_root. It may be empty iff all storage_receipts are valid or + /// state_root is an empty hash or if used in network genesis (e.g. during consensus chain init). + pub state: WriteLog, + + /// Storage receipts for the state root. The list may be empty or a signature in the list + /// invalid iff the state is non-empty or state_root is an empty hash or if used in network + /// genesis (e.g. during consensus chain init). + pub storage_receipts: Vec, + + /// Runtime round in the genesis. + pub round: u64, +}