Skip to content

Commit

Permalink
Merge pull request #5551 from oasisprotocol/peternose/feature/churp-c…
Browse files Browse the repository at this point in the history
…reate

go/keymanager/churp: Add create and update methods
  • Loading branch information
peternose authored Feb 13, 2024
2 parents c9ca776 + 111541a commit fd0b0d6
Show file tree
Hide file tree
Showing 18 changed files with 1,729 additions and 172 deletions.
1 change: 1 addition & 0 deletions .changelog/5551.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
go/keymanager/churp: Add create and update methods
20 changes: 20 additions & 0 deletions go/consensus/api/events/events.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,30 @@ package events
import (
"encoding/base64"
"fmt"
"sync"

"github.com/oasisprotocol/oasis-core/go/common/cbor"
)

// eventSeparator is the separator used to separate module from event name.
const eventSeparator = "."

// registeredEvents stores registered event names.
var registeredEvents sync.Map

// NewEventName creates a new event name.
//
// Module and event must be unique. If they are not, this method will panic.
func NewEventName(module string, event string) string {
// Check for duplicate event names.
name := module + eventSeparator + event
if _, isRegistered := registeredEvents.LoadOrStore(name, struct{}{}); isRegistered {
panic(fmt.Errorf("events: event already registered: %s", name))
}

return name
}

// Provable is an interface implemented by event types which can be proven.
type Provable interface {
// ShouldProve returns true iff the event should be included in the event proof tree.
Expand Down
67 changes: 67 additions & 0 deletions go/consensus/cometbft/apps/keymanager/churp/epoch.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package churp

import (
"fmt"

beacon "github.com/oasisprotocol/oasis-core/go/beacon/api"
tmapi "github.com/oasisprotocol/oasis-core/go/consensus/cometbft/api"
churpState "github.com/oasisprotocol/oasis-core/go/consensus/cometbft/apps/keymanager/churp/state"
registryState "github.com/oasisprotocol/oasis-core/go/consensus/cometbft/apps/registry/state"
"github.com/oasisprotocol/oasis-core/go/keymanager/churp"
)

func (ext *churpExt) onEpochChange(ctx *tmapi.Context, epoch beacon.EpochTime) error {
// Query the runtime and node lists.
state := churpState.NewMutableState(ctx.State())
regState := registryState.NewMutableState(ctx.State())
runtimes, _ := regState.Runtimes(ctx)

for _, rt := range runtimes {
statuses, err := state.Statuses(ctx, rt.ID)
if err != nil {
return fmt.Errorf("keymanager: churp: failed to fetch runtime statuses: %w", err)
}

for _, status := range statuses {
if status.NextHandoff == churp.HandoffsDisabled {
continue
}

switch epoch {
case status.NextHandoff:
// The epoch for the handoff just started, meaning that registrations
// are now closed. If not enough nodes applied for the next committee,
// we need to reset applications and start collecting again.
minCommitteeSize := int(status.Threshold)*2 + 1
if len(status.Applications) >= minCommitteeSize {
continue
}
case status.NextHandoff + 1:
// Handoff ended. Not all nodes replicated the secret and confirmed it,
// as otherwise the next handoff epoch would be updated.
// Reset and start collecting again
default:
continue
}

// The handoff failed, so postpone the round to the next epoch, giving
// nodes one epoch time to submit applications.
status.Applications = nil
status.Checksum = nil
status.NextHandoff = epoch + 1

if err := state.SetStatus(ctx, status); err != nil {
ctx.Logger().Error("keymanager: churp: failed to set status",
"err", err,
)
return fmt.Errorf("keymanager: churp: failed to set status: %w", err)
}

ctx.EmitEvent(tmapi.NewEventBuilder(ext.appName).TypedAttribute(&churp.UpdateEvent{
Status: status,
}))
}
}

return nil
}
78 changes: 78 additions & 0 deletions go/consensus/cometbft/apps/keymanager/churp/ext.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package churp

import (
"fmt"

"github.com/cometbft/cometbft/abci/types"

"github.com/oasisprotocol/oasis-core/go/common/cbor"
"github.com/oasisprotocol/oasis-core/go/consensus/api"
"github.com/oasisprotocol/oasis-core/go/consensus/api/transaction"
tmapi "github.com/oasisprotocol/oasis-core/go/consensus/cometbft/api"
genesis "github.com/oasisprotocol/oasis-core/go/genesis/api"
"github.com/oasisprotocol/oasis-core/go/keymanager/churp"
)

// Ensure that the CHURP extension implements the Extension interface.
var _ tmapi.Extension = (*churpExt)(nil)

type churpExt struct {
appName string
state tmapi.ApplicationState
}

// New creates a new CHRUP extension for the key manager application.
func New(appName string) tmapi.Extension {
return &churpExt{
appName: appName,
}
}

// Methods implements api.Extension.
func (ext *churpExt) Methods() []transaction.MethodName {
return churp.Methods
}

// OnRegister implements api.Extension.
func (ext *churpExt) OnRegister(state tmapi.ApplicationState, _ tmapi.MessageDispatcher) {
ext.state = state
}

// ExecuteTx implements api.Extension.
func (ext *churpExt) ExecuteTx(ctx *tmapi.Context, tx *transaction.Transaction) error {
switch tx.Method {
case churp.MethodCreate:
var cfg churp.CreateRequest
if err := cbor.Unmarshal(tx.Body, &cfg); err != nil {
return api.ErrInvalidArgument
}
return ext.create(ctx, &cfg)
case churp.MethodUpdate:
var cfg churp.UpdateRequest
if err := cbor.Unmarshal(tx.Body, &cfg); err != nil {
return api.ErrInvalidArgument
}
return ext.update(ctx, &cfg)
default:
panic(fmt.Sprintf("keymanager: churp: invalid method: %s", tx.Method))
}
}

// BeginBlock implements api.Extension.
func (ext *churpExt) BeginBlock(ctx *tmapi.Context) error {
changed, epoch := ext.state.EpochChanged(ctx)
if !changed {
return nil
}

return ext.onEpochChange(ctx, epoch)
}

// EndBlock implements api.Extension.
func (*churpExt) EndBlock(*tmapi.Context) error {
return nil
}

func (ext *churpExt) InitChain(*tmapi.Context, types.RequestInitChain, *genesis.Document) error {
return nil
}
167 changes: 167 additions & 0 deletions go/consensus/cometbft/apps/keymanager/churp/state/state.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
package state

import (
"context"
"fmt"

"github.com/oasisprotocol/oasis-core/go/common"
"github.com/oasisprotocol/oasis-core/go/common/cbor"
"github.com/oasisprotocol/oasis-core/go/common/keyformat"
consensus "github.com/oasisprotocol/oasis-core/go/consensus/api"
abciAPI "github.com/oasisprotocol/oasis-core/go/consensus/cometbft/api"
"github.com/oasisprotocol/oasis-core/go/keymanager/churp"
"github.com/oasisprotocol/oasis-core/go/storage/mkvs"
)

var (
// parametersKeyFmt is the consensus parameters key format.
//
// Value is CBOR-serialized churp.ConsensusParameters.
parametersKeyFmt = consensus.KeyFormat.New(0x74)

// statusKeyFmt is the status key format.
//
// Key format is: 0x75 <runtime-id> <churp-id>.
// Value is CBOR-serialized churp.Status.
statusKeyFmt = consensus.KeyFormat.New(0x75, keyformat.H(&common.Namespace{}), uint8(0))
)

// ImmutableState is a immutable state wrapper.
type ImmutableState struct {
is *abciAPI.ImmutableState
}

// ConsensusParameters returns the consensus parameters.
func (st *ImmutableState) ConsensusParameters(ctx context.Context) (*churp.ConsensusParameters, error) {
raw, err := st.is.Get(ctx, parametersKeyFmt.Encode())
if err != nil {
return nil, abciAPI.UnavailableStateError(err)
}
if raw == nil {
return nil, fmt.Errorf("cometbft/keymanager/churp: expected consensus parameters to be present in app state")
}

var params churp.ConsensusParameters
if err = cbor.Unmarshal(raw, &params); err != nil {
return nil, abciAPI.UnavailableStateError(err)
}
return &params, nil
}

// Status returns the CHURP status for the specified runtime and CHURP instance.
func (st *ImmutableState) Status(ctx context.Context, runtimeID common.Namespace, churpID uint8) (*churp.Status, error) {
data, err := st.is.Get(ctx, statusKeyFmt.Encode(&runtimeID, churpID))
if err != nil {
return nil, abciAPI.UnavailableStateError(err)
}
if data == nil {
return nil, churp.ErrNoSuchStatus
}

var status churp.Status
if err := cbor.Unmarshal(data, &status); err != nil {
return nil, abciAPI.UnavailableStateError(err)
}
return &status, nil
}

// Statuses returns the CHURP statuses for the specified runtime.
func (st *ImmutableState) Statuses(ctx context.Context, runtimeID common.Namespace) ([]*churp.Status, error) {
it := st.is.NewIterator(ctx)
defer it.Close()

// We need to pre-hash the runtime ID, so we can compare it below.
runtimeIDHash := keyformat.PreHashed(runtimeID.Hash())

var statuses []*churp.Status
for it.Seek(statusKeyFmt.Encode(&runtimeID)); it.Valid(); it.Next() {
var (
hash keyformat.PreHashed
churpID uint8
)
if !statusKeyFmt.Decode(it.Key(), &hash, &churpID) {
break
}
if runtimeIDHash != hash {
break
}

var status churp.Status
if err := cbor.Unmarshal(it.Value(), &status); err != nil {
return nil, abciAPI.UnavailableStateError(err)
}
statuses = append(statuses, &status)
}
if it.Err() != nil {
return nil, abciAPI.UnavailableStateError(it.Err())
}

return statuses, nil
}

// AllStatuses returns the CHURP statuses for all runtimes.
func (st *ImmutableState) AllStatuses(ctx context.Context) ([]*churp.Status, error) {
it := st.is.NewIterator(ctx)
defer it.Close()

var statuses []*churp.Status
for it.Seek(statusKeyFmt.Encode()); it.Valid(); it.Next() {
if !statusKeyFmt.Decode(it.Key()) {
break
}

var status churp.Status
if err := cbor.Unmarshal(it.Value(), &status); err != nil {
return nil, abciAPI.UnavailableStateError(err)
}
statuses = append(statuses, &status)
}
if it.Err() != nil {
return nil, abciAPI.UnavailableStateError(it.Err())
}

return statuses, nil
}

// NewImmutableState creates a new immutable state wrapper.
func NewImmutableState(ctx context.Context, state abciAPI.ApplicationQueryState, version int64) (*ImmutableState, error) {
is, err := abciAPI.NewImmutableState(ctx, state, version)
if err != nil {
return nil, err
}
return &ImmutableState{is}, nil
}

// MutableState is a mutable state wrapper.
type MutableState struct {
*ImmutableState

ms mkvs.KeyValueTree
}

// SetConsensusParameters updates the state using the provided consensus parameters.
//
// This method must only be called from InitChain or EndBlock contexts.
func (st *MutableState) SetConsensusParameters(ctx context.Context, params *churp.ConsensusParameters) error {
if err := st.is.CheckContextMode(ctx, []abciAPI.ContextMode{abciAPI.ContextInitChain, abciAPI.ContextEndBlock}); err != nil {
return err
}
err := st.ms.Insert(ctx, parametersKeyFmt.Encode(), cbor.Marshal(params))
return abciAPI.UnavailableStateError(err)
}

// SetStatus updates the state using the provided CHURP status.
func (st *MutableState) SetStatus(ctx context.Context, status *churp.Status) error {
err := st.ms.Insert(ctx, statusKeyFmt.Encode(&status.RuntimeID, status.ID), cbor.Marshal(status))
return abciAPI.UnavailableStateError(err)
}

// NewMutableState creates a new mutable state wrapper.
func NewMutableState(tree mkvs.KeyValueTree) *MutableState {
return &MutableState{
ImmutableState: &ImmutableState{
&abciAPI.ImmutableState{ImmutableKeyValueTree: tree},
},
ms: tree,
}
}
Loading

0 comments on commit fd0b0d6

Please sign in to comment.