Skip to content

Commit

Permalink
Merge pull request #5662 from oasisprotocol/kostko/feature/vault
Browse files Browse the repository at this point in the history
go/vault: Add simple consensus layer vault
  • Loading branch information
kostko authored May 7, 2024
2 parents fa9e98a + 4a9f73a commit dcaef17
Show file tree
Hide file tree
Showing 57 changed files with 4,770 additions and 43 deletions.
5 changes: 5 additions & 0 deletions .changelog/5662.breaking.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
go/vault: Add simple consensus layer vault

The vault service is a simple multi-sig where multiple parties vote to
perform actions on behalf of the vault account. This feature is disabled
by default and needs to be enabled via a governance vote.
4 changes: 4 additions & 0 deletions go/consensus/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import (
"github.com/oasisprotocol/oasis-core/go/storage/mkvs/checkpoint"
mkvsNode "github.com/oasisprotocol/oasis-core/go/storage/mkvs/node"
"github.com/oasisprotocol/oasis-core/go/storage/mkvs/syncer"
vault "github.com/oasisprotocol/oasis-core/go/vault/api"
)

const (
Expand Down Expand Up @@ -202,6 +203,9 @@ type ClientBackend interface {

// RootHash returns the roothash backend.
RootHash() roothash.Backend

// Vault returns the vault backend.
Vault() vault.Backend
}

// Block is a consensus block.
Expand Down
5 changes: 5 additions & 0 deletions go/consensus/api/grpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
scheduler "github.com/oasisprotocol/oasis-core/go/scheduler/api"
staking "github.com/oasisprotocol/oasis-core/go/staking/api"
"github.com/oasisprotocol/oasis-core/go/storage/mkvs/syncer"
vault "github.com/oasisprotocol/oasis-core/go/vault/api"
)

var (
Expand Down Expand Up @@ -928,6 +929,10 @@ func (c *consensusClient) RootHash() roothash.Backend {
return roothash.NewRootHashClient(c.conn)
}

func (c *consensusClient) Vault() vault.Backend {
return vault.NewVaultClient(c.conn)
}

// NewConsensusClient creates a new gRPC consensus client service.
func NewConsensusClient(c *grpc.ClientConn) ClientBackend {
return &consensusClient{
Expand Down
30 changes: 23 additions & 7 deletions go/consensus/cometbft/abci/messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,27 @@ func (md *messageDispatcher) Subscribe(kind interface{}, ms api.MessageSubscribe

// Implements api.MessageDispatcher.
func (md *messageDispatcher) Publish(ctx *api.Context, kind, msg interface{}) (interface{}, error) {
nSubs := len(md.subscriptions[kind])
if nSubs == 0 {
return nil, api.ErrNoSubscribers
}

var result interface{}
var errs error
var (
result interface{}
errs error
numSubscribers int
)
for _, ms := range md.subscriptions[kind] {
// Check whether the subscriber can be toggled.
if togMs, ok := ms.(api.TogglableMessageSubscriber); ok {
enabled, err := togMs.Enabled(ctx)
if err != nil {
errs = errors.Join(errs, err)
continue
}
if !enabled {
// If a subscriber is not enabled, skip it during dispatch.
continue
}
}
numSubscribers++

// Deliver the message.
if resp, err := ms.ExecuteMessage(ctx, kind, msg); err != nil {
errs = errors.Join(errs, err)
} else {
Expand All @@ -45,6 +58,9 @@ func (md *messageDispatcher) Publish(ctx *api.Context, kind, msg interface{}) (i
}
}
}
if numSubscribers == 0 {
return nil, api.ErrNoSubscribers
}
if errs != nil {
return nil, errs
}
Expand Down
17 changes: 16 additions & 1 deletion go/consensus/cometbft/abci/messages_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ var errTest = fmt.Errorf("error")

type testSubscriber struct {
msgs []int32
enabled bool
fail bool
noResult bool
}
Expand All @@ -50,6 +51,11 @@ func (s *testSubscriber) ExecuteMessage(_ *api.Context, _, msg interface{}) (int
}
}

// Implements api.TogglableMessageSubscriber.
func (s *testSubscriber) Enabled(_ *api.Context) (bool, error) {
return s.enabled, nil
}

func TestMessageDispatcher(t *testing.T) {
require := require.New(t)

Expand All @@ -65,10 +71,18 @@ func TestMessageDispatcher(t *testing.T) {
require.Equal(api.ErrNoSubscribers, err)
require.Nil(res, "Publish results should be empty")

// With a subscriber.
// With a disabled subscriber should behave same as with no subscribers.
var ms testSubscriber
md.Subscribe(testMessageA, &ms)
res, err = md.Publish(ctx, testMessageA, &testMessage{foo: 42})
require.Error(err, "Publish")
require.Equal(api.ErrNoSubscribers, err)
require.Nil(res, "Publish results should be empty")
require.Empty(ms.msgs, "no messages should be delivered when subscriber is disabled")

// With an enabled subscriber.
ms.enabled = true
res, err = md.Publish(ctx, testMessageA, &testMessage{foo: 42})
require.NoError(err, "Publish")
require.EqualValues(int32(42), res, "correct publish message result")
require.EqualValues([]int32{42}, ms.msgs, "correct messages should be delivered")
Expand All @@ -92,6 +106,7 @@ func TestMessageDispatcher(t *testing.T) {

// Multiple subscribers. Multiple subscribers returning results on the same message is an invariant violation.
ms2 := testSubscriber{
enabled: true,
noResult: true,
}
md.Subscribe(testMessageA, &ms2)
Expand Down
27 changes: 27 additions & 0 deletions go/consensus/cometbft/abci/mux.go
Original file line number Diff line number Diff line change
Expand Up @@ -877,6 +877,30 @@ func (mux *abciMux) doRegister(app api.Application) error {
return nil
}

// resolveAppForMethod resolves an application that should handle the given method.
func (mux *abciMux) resolveAppForMethod(ctx *api.Context, method transaction.MethodName) (api.Application, error) {
app, ok := mux.appsByMethod[method]
if !ok {
ctx.Logger().Debug("unknown method",
"method", method,
)
return nil, fmt.Errorf("mux: unknown method: %s", method)
}

// Check whether an application can be toggled.
if togApp, ok := app.(api.TogglableApplication); ok {
enabled, err := togApp.Enabled(ctx)
if err != nil {
return nil, err
}
if !enabled {
// If an application is not enabled, treat it as if the method does not exist.
return nil, fmt.Errorf("mux: unknown method: %s", method)
}
}
return app, nil
}

func (mux *abciMux) rebuildAppLexOrdering() {
numApps := len(mux.appsByName)
appOrder := make([]string, 0, numApps)
Expand Down Expand Up @@ -962,6 +986,9 @@ func newABCIMux(ctx context.Context, upgrader upgrade.Backend, cfg *ApplicationC
appsByMethod: make(map[transaction.MethodName]api.Application),
}

// Subscribe message handlers.
mux.md.Subscribe(api.MessageExecuteSubcall, mux)

mux.logger.Debug("ABCI multiplexer initialized",
"block_height", state.BlockHeight(),
"state_root_hash", hex.EncodeToString(state.StateRootHash()),
Expand Down
56 changes: 56 additions & 0 deletions go/consensus/cometbft/abci/subcall.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package abci

import (
"fmt"

"github.com/oasisprotocol/oasis-core/go/consensus/api/transaction"
"github.com/oasisprotocol/oasis-core/go/consensus/cometbft/api"
)

// maxSubcallDepth is the maximum subcall depth.
const maxSubcallDepth = 8

// ExecuteMessage implements api.MessageSubscriber.
func (mux *abciMux) ExecuteMessage(ctx *api.Context, kind, msg interface{}) (interface{}, error) {
switch kind {
case api.MessageExecuteSubcall:
// Subcall execution request.
info, ok := msg.(*api.SubcallInfo)
if !ok {
return nil, fmt.Errorf("invalid subcall info")
}
return struct{}{}, mux.executeSubcall(ctx, info)
default:
return nil, nil
}
}

// executeSubcall executes a subcall.
func (mux *abciMux) executeSubcall(ctx *api.Context, info *api.SubcallInfo) error {
if ctx.CallDepth() > maxSubcallDepth {
return fmt.Errorf("call depth exceeded")
}

ctx = ctx.WithCallerAddress(info.Caller)
defer ctx.Close()
ctx = ctx.NewTransaction()
defer ctx.Close()

// Lookup method handler.
app, err := mux.resolveAppForMethod(ctx, info.Method)
if err != nil {
return err
}

tx := &transaction.Transaction{
Method: info.Method,
Body: info.Body,
}
if err = app.ExecuteTx(ctx, tx); err != nil {
return err
}

ctx.Commit()

return nil
}
10 changes: 3 additions & 7 deletions go/consensus/cometbft/abci/transaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,9 @@ func (mux *abciMux) processTx(ctx *api.Context, tx *transaction.Transaction, txS
}

// Lookup method handler.
app := mux.appsByMethod[tx.Method]
if app == nil {
ctx.Logger().Debug("unknown method",
"tx", tx,
"method", tx.Method,
)
return fmt.Errorf("mux: unknown method: %s", tx.Method)
app, err := mux.resolveAppForMethod(ctx, tx.Method)
if err != nil {
return err
}

// Pass the transaction through the fee handler if configured.
Expand Down
12 changes: 9 additions & 3 deletions go/consensus/cometbft/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -374,9 +374,15 @@ func (bsc *BaseServiceClient) DeliverCommand(context.Context, int64, interface{}

type messageKind uint8

// MessageStateSyncCompleted is the message kind for when the node successfully performs a state
// sync. The message itself is nil.
var MessageStateSyncCompleted = messageKind(0)
var (
// MessageStateSyncCompleted is the message kind for when the node successfully performs a state
// sync. The message itself is nil.
MessageStateSyncCompleted = messageKind(0)

// MessageExecuteSubcall is the message kind for requesting subcall execution. The message is
// handled by the multiplexer and should be an instance of SubcallInfo.
MessageExecuteSubcall = messageKind(1)
)

// CometBFTChainID returns the CometBFT chain ID computed from chain context.
func CometBFTChainID(chainContext string) string {
Expand Down
12 changes: 12 additions & 0 deletions go/consensus/cometbft/api/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ type MessageSubscriber interface {
ExecuteMessage(ctx *Context, kind, msg interface{}) (interface{}, error)
}

// TogglableMessageSubscriber is a message subscriber that can be disabled.
type TogglableMessageSubscriber interface {
// Enabled checks whether the message subscriber is enabled.
Enabled(ctx *Context) (bool, error)
}

// MessageDispatcher is a message dispatcher interface.
type MessageDispatcher interface {
// Subscribe subscribes a given message subscriber to messages of a specific kind.
Expand Down Expand Up @@ -134,3 +140,9 @@ type Extension interface {
// Note: Errors are irrecoverable and will result in a panic.
EndBlock(*Context) error
}

// TogglableApplication is an application that can be disabled.
type TogglableApplication interface {
// Enabled checks whether the application is enabled.
Enabled(*Context) (bool, error)
}
8 changes: 8 additions & 0 deletions go/consensus/cometbft/api/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,14 @@ func (c *Context) CallerAddress() staking.Address {
return c.callerAddress
}

// CallDepth returns the call depth.
func (c *Context) CallDepth() int {
if c.parent == nil {
return 0
}
return c.parent.CallDepth() + 1
}

// NewChild creates a new child context that shares state with the current context.
//
// If you want isolated state and events use NewTransaction instad.
Expand Down
6 changes: 6 additions & 0 deletions go/consensus/cometbft/api/context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ func TestChildContext(t *testing.T) {

ctx.SetTxSigner(pk1)
require.Equal(addr1, ctx.CallerAddress(), "CallerAddress should correspond to TxSigner")
require.EqualValues(0, ctx.CallDepth(), "CallDepth should be zero for top-level context")

pk2 := signature.NewPublicKey("1234567890000000000000000000000000000000000000000000000000000000")
addr2 := staking.NewAddress(pk2)
Expand All @@ -64,6 +65,7 @@ func TestChildContext(t *testing.T) {
require.EqualValues(ctx.InitialHeight(), child.InitialHeight(), "child.InitialHeight should correspond to parent.InitialHeight")
require.EqualValues(ctx.BlockHeight(), child.BlockHeight(), "child.BlockHeight should correspond to parent.BlockHeight")
require.EqualValues(ctx.BlockContext(), child.BlockContext(), "child.BlockContext should correspond to parent.BlockContext")
require.EqualValues(1, child.CallDepth(), "child.CallDepth should be correct")

// Emitting an event should not propagate to the parent immediately.
child.EmitEvent(NewEventBuilder("test").TypedAttribute(&FooEvent{Bar: []byte("bar")}))
Expand Down Expand Up @@ -144,6 +146,8 @@ func TestNestedTransactionContext(t *testing.T) {
require := require.New(t)

doChild2 := func(ctx *Context) {
require.EqualValues(2, ctx.CallDepth(), "CallDepth should be correct")

tree := ctx.State()

err := tree.Insert(ctx, []byte("child2"), []byte("value2"))
Expand All @@ -163,6 +167,8 @@ func TestNestedTransactionContext(t *testing.T) {
}

doChild1 := func(ctx *Context) {
require.EqualValues(1, ctx.CallDepth(), "CallDepth should be correct")

tree := ctx.State()

err := tree.Insert(ctx, []byte("child1"), []byte("value1"))
Expand Down
17 changes: 17 additions & 0 deletions go/consensus/cometbft/api/subcall.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package api

import (
"github.com/oasisprotocol/oasis-core/go/common/cbor"
"github.com/oasisprotocol/oasis-core/go/consensus/api/transaction"
staking "github.com/oasisprotocol/oasis-core/go/staking/api"
)

// SubcallInfo is the information about a subcall that should be executed.
type SubcallInfo struct {
// Caller is the address of the caller.
Caller staking.Address
// Method is the name of the method that should be invoked.
Method transaction.MethodName
// Body is the subcall body.
Body cbor.RawMessage
}
6 changes: 3 additions & 3 deletions go/consensus/cometbft/apps/governance/transactions.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ func (app *governanceApplication) submitProposal(
}

// Load submitter account.
submitterAddr := stakingAPI.NewAddress(ctx.TxSigner())
submitterAddr := ctx.CallerAddress()
if !submitterAddr.IsValid() {
return nil, stakingAPI.ErrForbidden
}
Expand Down Expand Up @@ -239,7 +239,7 @@ func (app *governanceApplication) castVote(
return nil
}

submitterAddr := stakingAPI.NewAddress(ctx.TxSigner())
submitterAddr := ctx.CallerAddress()
if !submitterAddr.IsValid() {
return stakingAPI.ErrForbidden
}
Expand Down Expand Up @@ -304,7 +304,7 @@ func (app *governanceApplication) castVote(

if !eligible {
ctx.Logger().Debug("governance: submitter not eligible to vote",
"submitter", ctx.TxSigner(),
"submitter", ctx.CallerAddress(),
)
return governance.ErrNotEligible
}
Expand Down
Loading

0 comments on commit dcaef17

Please sign in to comment.