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

go/keymanager/churp: Allow nodes to apply for a new committee #5568

Merged
merged 5 commits into from
Feb 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .changelog/5551.feature.md
Original file line number Diff line number Diff line change
@@ -1 +1 @@
go/keymanager/churp: Add create and update methods
go/keymanager/churp: Allow key managers to create/update scheme
1 change: 1 addition & 0 deletions .changelog/5568.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
go/keymanager/churp: Allow nodes to apply for a new committee
5 changes: 3 additions & 2 deletions go/consensus/cometbft/apps/keymanager/churp/epoch.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,12 @@ func (ext *churpExt) onEpochChange(ctx *tmapi.Context, epoch beacon.EpochTime) e
continue
}

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

if err := state.SetStatus(ctx, status); err != nil {
ctx.Logger().Error("keymanager: churp: failed to set status",
Expand Down
6 changes: 6 additions & 0 deletions go/consensus/cometbft/apps/keymanager/churp/ext.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ func (ext *churpExt) ExecuteTx(ctx *tmapi.Context, tx *transaction.Transaction)
return api.ErrInvalidArgument
}
return ext.update(ctx, &cfg)
case churp.MethodApply:
var reg churp.SignedApplicationRequest
if err := cbor.Unmarshal(tx.Body, &reg); err != nil {
return api.ErrInvalidArgument
}
return ext.apply(ctx, &reg)
default:
panic(fmt.Sprintf("keymanager: churp: invalid method: %s", tx.Method))
}
Expand Down
36 changes: 36 additions & 0 deletions go/consensus/cometbft/apps/keymanager/churp/query.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package churp

import (
"context"

"github.com/oasisprotocol/oasis-core/go/common"
churpState "github.com/oasisprotocol/oasis-core/go/consensus/cometbft/apps/keymanager/churp/state"
"github.com/oasisprotocol/oasis-core/go/keymanager/churp"
)

// Query is the key manager query interface.
type Query interface {
Status(context.Context, common.Namespace, uint8) (*churp.Status, error)
Statuses(context.Context, common.Namespace) ([]*churp.Status, error)
AllStatuses(context.Context) ([]*churp.Status, error)
}

type querier struct {
state *churpState.ImmutableState
}

func (kq *querier) Status(ctx context.Context, runtimeID common.Namespace, churpID uint8) (*churp.Status, error) {
return kq.state.Status(ctx, runtimeID, churpID)
}

func (kq *querier) Statuses(ctx context.Context, runtimeID common.Namespace) ([]*churp.Status, error) {
return kq.state.Statuses(ctx, runtimeID)
}

func (kq *querier) AllStatuses(ctx context.Context) ([]*churp.Status, error) {
return kq.state.AllStatuses(ctx)
}

func NewQuery(state *churpState.ImmutableState) Query {
return &querier{state}
}
122 changes: 122 additions & 0 deletions go/consensus/cometbft/apps/keymanager/churp/state/interop/interop.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package interop

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/crypto/hash"
"github.com/oasisprotocol/oasis-core/go/common/crypto/signature"
memorySigner "github.com/oasisprotocol/oasis-core/go/common/crypto/signature/signers/memory"
"github.com/oasisprotocol/oasis-core/go/common/sgx"
churpState "github.com/oasisprotocol/oasis-core/go/consensus/cometbft/apps/keymanager/churp/state"
"github.com/oasisprotocol/oasis-core/go/keymanager/churp"
"github.com/oasisprotocol/oasis-core/go/keymanager/secrets"
"github.com/oasisprotocol/oasis-core/go/storage/mkvs"
)

// InitializeTestKeyManagerSecretsState must be kept in sync with tests in runtimes/consensus/state/keymanager/churp.rs.
func InitializeTestKeyManagerSecretsState(ctx context.Context, mkvs mkvs.Tree) error {
state := churpState.NewMutableState(mkvs)

// One runtime.
var runtime common.Namespace
if err := runtime.UnmarshalHex("8000000000000000000000000000000000000000000000000000000000000000"); err != nil {
return err
}

// Two enclave identities.
var enclave1, enclave2 sgx.EnclaveIdentity
if err := enclave1.MrEnclave.UnmarshalHex("c9a589851b1f35627177fd70378ed778170f737611e4dfbf0b6d25bdff55b474"); err != nil {
return err
}
if err := enclave1.MrSigner.UnmarshalHex("7d310664780931ae103ab30a90171c201af385a72757bb4683578fdebde9adf5"); err != nil {
return err
}
if err := enclave2.MrEnclave.UnmarshalHex("756eaf76f5482c5345808b1eaccdd5c60f864bb2aa2d2b870df00ce435af4e23"); err != nil {
return err
}
if err := enclave2.MrSigner.UnmarshalHex("3597a2ff0743016f28e5d7e129304ee1c43dbdae3dba94e19cee3549038a5a32"); err != nil {
return err
}

// CHURP identity.
identity := churp.Identity{
ID: 1,
RuntimeID: runtime,
}

// Signed policy.
policy := churp.PolicySGX{
Identity: identity,
Serial: 6,
MayShare: []sgx.EnclaveIdentity{enclave1},
MayJoin: []sgx.EnclaveIdentity{enclave2},
}
sigPolicy := churp.SignedPolicySGX{
Policy: policy,
Signatures: []signature.Signature{},
}

// Two signers.
signers := []signature.Signer{
memorySigner.NewTestSigner("first signer"),
memorySigner.NewTestSigner("second signer"),
}

for _, signer := range signers {
sig, err := signature.Sign(signer, secrets.PolicySGXSignatureContext, cbor.Marshal(policy))
if err != nil {
return fmt.Errorf("failed to sign policy: %w", err)
}
sigPolicy.Signatures = append(sigPolicy.Signatures, *sig)
}

// Random checksum.
var checksum hash.Hash
if err := checksum.UnmarshalHex("1bff211fae98c88ba82388ae954b88a71d3bbe327e162e9fa711fe7a1b759c3e"); err != nil {
return err
}

// Committee.
committee := []signature.PublicKey{signers[0].Public(), signers[1].Public()}

// Applications.
applications := map[signature.PublicKey]churp.Application{
signers[0].Public(): {
Checksum: checksum,
Reconstructed: false,
},
signers[1].Public(): {
Checksum: checksum,
Reconstructed: true,
},
}

// Empty status.
var status churp.Status
if err := state.SetStatus(ctx, &status); err != nil {
return fmt.Errorf("failed to set key CHURP status: %w", err)
}

// Non-empty status.
status = churp.Status{
Identity: identity,
GroupID: churp.EccNistP384,
Threshold: 2,
Round: 3,
NextHandoff: 4,
HandoffInterval: 5,
Policy: sigPolicy,
Committee: committee,
Applications: applications,
Checksum: &checksum,
}

if err := state.SetStatus(ctx, &status); err != nil {
return fmt.Errorf("failed to set key CHURP status: %w", err)
}

return nil
}
111 changes: 111 additions & 0 deletions go/consensus/cometbft/apps/keymanager/churp/txs.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@ import (
"fmt"

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

Expand Down Expand Up @@ -192,6 +195,114 @@ func (ext *churpExt) update(ctx *tmapi.Context, req *churp.UpdateRequest) error
return nil
}

func (ext *churpExt) apply(ctx *tmapi.Context, req *churp.SignedApplicationRequest) error {
// Prepare states.
state := churpState.NewMutableState(ctx.State())
regState := registryState.NewMutableState(ctx.State())

// Ensure that the runtime exists and is a key manager.
kmRt, err := common.KeyManagerRuntime(ctx, req.Application.RuntimeID)
if err != nil {
return err
}

// Get the existing status.
status, err := state.Status(ctx, req.Application.RuntimeID, req.Application.ID)
if err != nil {
return fmt.Errorf("keymanager: churp: non-existing ID: %d", req.Application.ID)
}

// Allow applications one epoch before the next handoff.
now, err := ext.state.GetCurrentEpoch(ctx)
if err != nil {
return err
}

switch status.NextHandoff {
case churp.HandoffsDisabled:
return fmt.Errorf("keymanager: churp: handoffs disabled")
case now + 1:
default:
return fmt.Errorf("keymanager: churp: submissions closed")
}

if status.Round != req.Application.Round {
return fmt.Errorf("keymanager: churp: invalid round: got %d, expected %d", req.Application.Round, status.Round)
}

// Allow only one application per round, to ensure the node's
// verification matrix (commitment) doesn't change.
nodeID := ctx.TxSigner()
if _, ok := status.Applications[nodeID]; ok {
return fmt.Errorf("keymanager: churp: application already submitted")
}

// Verify the node.
n, err := regState.Node(ctx, nodeID)
if err != nil {
return err
}
if n.IsExpired(uint64(now)) {
return fmt.Errorf("keymanager: churp: node registration expired")
}
if !n.HasRoles(node.RoleKeyManager) {
return fmt.Errorf("keymanager: churp: node not key manager")
}

// Verify RAK signature.
nodeRt, err := common.NodeRuntime(n, kmRt.ID)
if err != nil {
return err
}
rak, err := common.RuntimeAttestationKey(nodeRt, kmRt)
if err != nil {
return fmt.Errorf("keymanager: churp: failed to fetch node's rak: %w", err)
}
if err = req.VerifyRAK(rak); err != nil {
return fmt.Errorf("keymanager: churp: invalid signature: %w", err)
}

if ctx.IsCheckOnly() {
return nil
}

// Charge gas for this operation.
kmParams, err := state.ConsensusParameters(ctx)
if err != nil {
return err
}
if err = ctx.Gas().UseGas(1, churp.GasOpApply, kmParams.GasCosts); err != nil {
return err
}

// Return early if simulating since this is just estimating gas.
if ctx.IsSimulation() {
return nil
}

// Ok, as far as we can tell the application is valid, apply it.
if status.Applications == nil {
status.Applications = make(map[signature.PublicKey]churp.Application)
}
status.Applications[nodeID] = churp.Application{
Checksum: req.Application.Checksum,
Reconstructed: false,
}

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
}

func (ext *churpExt) computeNextHandoff(ctx *tmapi.Context) (beacon.EpochTime, error) {
// The next handoff will start at the beginning of the next epoch,
// meaning that nodes need to send their applications until the end
Expand Down
Loading
Loading