From d4a6617756dd5b01d9c5d7f2b7842de102178e4e Mon Sep 17 00:00:00 2001 From: Jernej Kos Date: Thu, 12 Nov 2020 14:53:59 +0100 Subject: [PATCH] WIP: Add staking runtime messages --- .../tendermint/apps/roothash/api/api.go | 9 +- .../tendermint/apps/roothash/messages.go | 10 +- .../tendermint/apps/roothash/transactions.go | 6 + .../tendermint/apps/staking/staking.go | 22 +- .../tendermint/apps/staking/transactions.go | 14 +- .../cmd/debug/txsource/workload/runtime.go | 210 +++++++++++++++++- .../cmd/debug/txsource/workload/workload.go | 5 + go/roothash/api/block/message.go | 29 ++- go/roothash/api/block/message_test.go | 5 + runtime/src/common/mod.rs | 1 + runtime/src/common/roothash.rs | 32 ++- runtime/src/common/staking.rs | 23 ++ tests/clients/simple-keyvalue-enc/src/main.rs | 2 +- tests/clients/simple-keyvalue-ops/src/main.rs | 2 +- tests/clients/simple-keyvalue/src/main.rs | 2 +- tests/clients/test-long-term/src/main.rs | 2 +- tests/runtimes/simple-keyvalue/api/src/api.rs | 20 +- tests/runtimes/simple-keyvalue/api/src/lib.rs | 2 +- tests/runtimes/simple-keyvalue/src/main.rs | 80 +++++-- 19 files changed, 433 insertions(+), 43 deletions(-) create mode 100644 runtime/src/common/staking.rs diff --git a/go/consensus/tendermint/apps/roothash/api/api.go b/go/consensus/tendermint/apps/roothash/api/api.go index 92d8b401b81..9dc9741c4fe 100644 --- a/go/consensus/tendermint/apps/roothash/api/api.go +++ b/go/consensus/tendermint/apps/roothash/api/api.go @@ -3,5 +3,10 @@ package api type messageKind uint8 -// RuntimeMessageNoop is the message kind used when dispatching Noop runtime messages. -var RuntimeMessageNoop = messageKind(0) +var ( + // RuntimeMessageNoop is the message kind used when dispatching Noop runtime messages. + RuntimeMessageNoop = messageKind(0) + + // RuntimeMessageStaking is the message kind used when dispatching Staking runtime messages. + RuntimeMessageStaking = messageKind(1) +) diff --git a/go/consensus/tendermint/apps/roothash/messages.go b/go/consensus/tendermint/apps/roothash/messages.go index f68339e0aaf..4a127bc20c2 100644 --- a/go/consensus/tendermint/apps/roothash/messages.go +++ b/go/consensus/tendermint/apps/roothash/messages.go @@ -7,6 +7,7 @@ import ( roothashApi "github.com/oasisprotocol/oasis-core/go/consensus/tendermint/apps/roothash/api" roothash "github.com/oasisprotocol/oasis-core/go/roothash/api" "github.com/oasisprotocol/oasis-core/go/roothash/api/block" + staking "github.com/oasisprotocol/oasis-core/go/staking/api" ) func (app *rootHashApplication) processRuntimeMessages( @@ -14,11 +15,18 @@ func (app *rootHashApplication) processRuntimeMessages( rtState *roothash.RuntimeState, msgs []block.Message, ) error { + // Prepare a new context for processing messages. + msgCtx := ctx.WithCallerAddress(staking.NewRuntimeAddress(rtState.Runtime.ID)) + msgCtx.SetGasAccountant(tmapi.NewNopGasAccountant()) // Gas was already accounted for. + defer msgCtx.Close() + for i, msg := range msgs { var err error switch { case msg.Noop != nil: - err = app.md.Publish(ctx, roothashApi.RuntimeMessageNoop, &msg.Noop) + err = app.md.Publish(msgCtx, roothashApi.RuntimeMessageNoop, msg.Noop) + case msg.Staking != nil: + err = app.md.Publish(msgCtx, roothashApi.RuntimeMessageStaking, msg.Staking) default: // Unsupported message. err = roothash.ErrInvalidArgument diff --git a/go/consensus/tendermint/apps/roothash/transactions.go b/go/consensus/tendermint/apps/roothash/transactions.go index eb6be17f39d..9ac291e1434 100644 --- a/go/consensus/tendermint/apps/roothash/transactions.go +++ b/go/consensus/tendermint/apps/roothash/transactions.go @@ -205,6 +205,12 @@ func (app *rootHashApplication) executorCommit( return err } + /* + msgGasAccountant := func(msg *block.Message) { + // TODO: Charging for gas requires each message to know how to charge for gas? + } + */ + for _, commit := range cc.Commits { if err = rtState.ExecutorPool.AddExecutorCommitment(ctx, rtState.CurrentBlock, sv, nl, &commit); err != nil { // nolint: gosec ctx.Logger().Error("failed to add compute commitment to round", diff --git a/go/consensus/tendermint/apps/staking/staking.go b/go/consensus/tendermint/apps/staking/staking.go index c1e609ace31..a95fb8e928e 100644 --- a/go/consensus/tendermint/apps/staking/staking.go +++ b/go/consensus/tendermint/apps/staking/staking.go @@ -11,8 +11,10 @@ import ( "github.com/oasisprotocol/oasis-core/go/consensus/api/transaction" "github.com/oasisprotocol/oasis-core/go/consensus/tendermint/api" registryState "github.com/oasisprotocol/oasis-core/go/consensus/tendermint/apps/registry/state" + roothashApi "github.com/oasisprotocol/oasis-core/go/consensus/tendermint/apps/roothash/api" stakingState "github.com/oasisprotocol/oasis-core/go/consensus/tendermint/apps/staking/state" epochtime "github.com/oasisprotocol/oasis-core/go/epochtime/api" + "github.com/oasisprotocol/oasis-core/go/roothash/api/block" staking "github.com/oasisprotocol/oasis-core/go/staking/api" ) @@ -44,6 +46,9 @@ func (app *stakingApplication) Dependencies() []string { func (app *stakingApplication) OnRegister(state api.ApplicationState, md api.MessageDispatcher) { app.state = state + + // Subscribe to messages emitted by other apps. + md.Subscribe(roothashApi.RuntimeMessageStaking, app) } func (app *stakingApplication) OnCleanup() { @@ -99,7 +104,22 @@ func (app *stakingApplication) BeginBlock(ctx *api.Context, request types.Reques } func (app *stakingApplication) ExecuteMessage(ctx *api.Context, kind, msg interface{}) error { - return staking.ErrInvalidArgument + state := stakingState.NewMutableState(ctx.State()) + + switch kind { + case roothashApi.RuntimeMessageStaking: + m := msg.(*block.StakingMessage) + switch { + case m.Transfer != nil: + return app.transfer(ctx, state, m.Transfer) + case m.Withdraw != nil: + return app.withdraw(ctx, state, m.Withdraw) + default: + return staking.ErrInvalidArgument + } + default: + return staking.ErrInvalidArgument + } } func (app *stakingApplication) ExecuteTx(ctx *api.Context, tx *transaction.Transaction) error { diff --git a/go/consensus/tendermint/apps/staking/transactions.go b/go/consensus/tendermint/apps/staking/transactions.go index b38c825e1d1..876b0d57db4 100644 --- a/go/consensus/tendermint/apps/staking/transactions.go +++ b/go/consensus/tendermint/apps/staking/transactions.go @@ -35,7 +35,7 @@ func (app *stakingApplication) transfer(ctx *api.Context, state *stakingState.Mu return err } - fromAddr := staking.NewAddress(ctx.TxSigner()) + fromAddr := ctx.CallerAddress() if fromAddr.IsReserved() || !isTransferPermitted(params, fromAddr) { return staking.ErrForbidden } @@ -114,7 +114,7 @@ func (app *stakingApplication) burn(ctx *api.Context, state *stakingState.Mutabl return err } - fromAddr := staking.NewAddress(ctx.TxSigner()) + fromAddr := ctx.CallerAddress() if fromAddr.IsReserved() { return staking.ErrForbidden } @@ -180,7 +180,7 @@ func (app *stakingApplication) addEscrow(ctx *api.Context, state *stakingState.M return staking.ErrInvalidArgument } - fromAddr := staking.NewAddress(ctx.TxSigner()) + fromAddr := ctx.CallerAddress() if fromAddr.IsReserved() { return staking.ErrForbidden } @@ -272,7 +272,7 @@ func (app *stakingApplication) reclaimEscrow(ctx *api.Context, state *stakingSta return err } - toAddr := staking.NewAddress(ctx.TxSigner()) + toAddr := ctx.CallerAddress() if toAddr.IsReserved() { return staking.ErrForbidden } @@ -396,7 +396,7 @@ func (app *stakingApplication) amendCommissionSchedule( return err } - fromAddr := staking.NewAddress(ctx.TxSigner()) + fromAddr := ctx.CallerAddress() if fromAddr.IsReserved() { return staking.ErrForbidden } @@ -445,7 +445,7 @@ func (app *stakingApplication) allow( } // Validate addresses -- if either is reserved or both are equal, the method should fail. - addr := staking.NewAddress(ctx.TxSigner()) + addr := ctx.CallerAddress() if addr.IsReserved() || allow.Beneficiary.IsReserved() { return staking.ErrForbidden } @@ -529,7 +529,7 @@ func (app *stakingApplication) withdraw( } // Validate addresses -- if either is reserved or both are equal, the method should fail. - toAddr := staking.NewAddress(ctx.TxSigner()) + toAddr := ctx.CallerAddress() if toAddr.IsReserved() || withdraw.From.IsReserved() { return staking.ErrForbidden } diff --git a/go/oasis-node/cmd/debug/txsource/workload/runtime.go b/go/oasis-node/cmd/debug/txsource/workload/runtime.go index 31eb31687db..1eef2289a13 100644 --- a/go/oasis-node/cmd/debug/txsource/workload/runtime.go +++ b/go/oasis-node/cmd/debug/txsource/workload/runtime.go @@ -17,6 +17,7 @@ import ( consensus "github.com/oasisprotocol/oasis-core/go/consensus/api" runtimeClient "github.com/oasisprotocol/oasis-core/go/runtime/client/api" runtimeTransaction "github.com/oasisprotocol/oasis-core/go/runtime/transaction" + staking "github.com/oasisprotocol/oasis-core/go/staking/api" ) // NameRuntime is the name of the runtime workload. @@ -45,18 +46,22 @@ const ( type runtimeRequest uint8 const ( - runtimeRequestInsert runtimeRequest = 0 - runtimeRequestGet runtimeRequest = 1 - runtimeRequestRemove runtimeRequest = 2 - runtimeRequestMessage runtimeRequest = 3 + runtimeRequestInsert runtimeRequest = 0 + runtimeRequestGet runtimeRequest = 1 + runtimeRequestRemove runtimeRequest = 2 + runtimeRequestMessage runtimeRequest = 3 + runtimeRequestWithdraw runtimeRequest = 4 + runtimeRequestTransfer runtimeRequest = 5 ) // Weights to select between requests types. var runtimeRequestWeights = map[runtimeRequest]int{ - runtimeRequestInsert: 2, - runtimeRequestGet: 1, - runtimeRequestRemove: 2, - runtimeRequestMessage: 1, + runtimeRequestInsert: 3, + runtimeRequestGet: 2, + runtimeRequestRemove: 3, + runtimeRequestMessage: 1, + runtimeRequestWithdraw: 1, + runtimeRequestTransfer: 1, } // RuntimeFlags are the runtime workload flags. @@ -67,6 +72,10 @@ type runtime struct { runtimeID common.Namespace reckonedKeyValueState map[string]string + + testAddress staking.Address + testBalance quantity.Quantity + runtimeBalance quantity.Quantity } func (r *runtime) generateVal(rng *rand.Rand, existingKey bool) string { @@ -311,9 +320,177 @@ func (r *runtime) doMessageRequest(ctx context.Context, rng *rand.Rand, rtc runt return nil } +func (r *runtime) doWithdrawRequest(ctx context.Context, rng *rand.Rand, rtc runtimeClient.RuntimeClient) error { + // Submit message request. + amount := *quantity.NewFromUint64(1) + req := &runtimeTransaction.TxnCall{ + Method: "consensus_withdraw", + Args: struct { + Withdraw staking.Withdraw `json:"withdraw"` + Nonce uint64 `json:"nonce"` + }{ + Withdraw: staking.Withdraw{ + From: r.testAddress, + Amount: amount, + }, + Nonce: rng.Uint64(), + }, + } + rsp, err := r.submitRuntimeRquest(ctx, rtc, req) + if err != nil { + r.Logger.Error("Submit withdraw request failure", + "request", req, + "err", err, + ) + return fmt.Errorf("submit withdraw request failed: %w", err) + } + + r.Logger.Debug("withdraw request success", + "request", req, + "response", rsp, + ) + + // Make sure the withdrawal was processed correctly in the consensus layer. + acct, err := r.Consensus().Staking().Account(ctx, &staking.OwnerQuery{ + Height: consensus.HeightLatest, + Owner: r.testAddress, + }) + if err != nil { + return fmt.Errorf("failed to query test account: %w", err) + } + + // Check source account balance. + if err = r.testBalance.Sub(&amount); err != nil { + return fmt.Errorf("failed to compute new test balance: %w", err) + } + if r.testBalance.Cmp(&acct.General.Balance) != 0 { + return fmt.Errorf("unexpected balance in test account (expected: %s got: %s)", r.testBalance, acct.General.Balance) + } + + // Check runtime account balance. + acct, err = r.Consensus().Staking().Account(ctx, &staking.OwnerQuery{ + Height: consensus.HeightLatest, + Owner: staking.NewRuntimeAddress(r.runtimeID), + }) + if err != nil { + return fmt.Errorf("failed to query runtime account: %w", err) + } + + if err = r.runtimeBalance.Add(&amount); err != nil { + return fmt.Errorf("failed to compute new runtime balance: %w", err) + } + if r.runtimeBalance.Cmp(&acct.General.Balance) != 0 { + return fmt.Errorf("unexpected balance in runtime account (expected: %s got: %s)", r.runtimeBalance, acct.General.Balance) + } + + return nil +} + +func (r *runtime) doTransferRequest(ctx context.Context, rng *rand.Rand, rtc runtimeClient.RuntimeClient) error { + if r.runtimeBalance.IsZero() { + return nil + } + + // Submit message request. + amount := *quantity.NewFromUint64(1) + req := &runtimeTransaction.TxnCall{ + Method: "consensus_transfer", + Args: struct { + Transfer staking.Transfer `json:"transfer"` + Nonce uint64 `json:"nonce"` + }{ + Transfer: staking.Transfer{ + To: r.testAddress, + Amount: amount, + }, + Nonce: rng.Uint64(), + }, + } + rsp, err := r.submitRuntimeRquest(ctx, rtc, req) + if err != nil { + r.Logger.Error("Submit transfer request failure", + "request", req, + "err", err, + ) + return fmt.Errorf("submit transfer request failed: %w", err) + } + + r.Logger.Debug("transfer request success", + "request", req, + "response", rsp, + ) + + // Make sure the transfer was processed correctly in the consensus layer. + acct, err := r.Consensus().Staking().Account(ctx, &staking.OwnerQuery{ + Height: consensus.HeightLatest, + Owner: r.testAddress, + }) + if err != nil { + return fmt.Errorf("failed to query test account: %w", err) + } + + // Check source account balance. + if err = r.testBalance.Add(&amount); err != nil { + return fmt.Errorf("failed to compute new test balance: %w", err) + } + if r.testBalance.Cmp(&acct.General.Balance) != 0 { + return fmt.Errorf("unexpected balance in test account (expected: %s got: %s)", r.testBalance, acct.General.Balance) + } + + // Check runtime account balance. + acct, err = r.Consensus().Staking().Account(ctx, &staking.OwnerQuery{ + Height: consensus.HeightLatest, + Owner: staking.NewRuntimeAddress(r.runtimeID), + }) + if err != nil { + return fmt.Errorf("failed to query runtime account: %w", err) + } + + if err = r.runtimeBalance.Sub(&amount); err != nil { + return fmt.Errorf("failed to compute new runtime balance: %w", err) + } + if r.runtimeBalance.Cmp(&acct.General.Balance) != 0 { + return fmt.Errorf("unexpected balance in runtime account (expected: %s got: %s)", r.runtimeBalance, acct.General.Balance) + } + + return nil +} + // Implements Workload. func (r *runtime) NeedsFunds() bool { - return false + return true +} + +func (r *runtime) initAccounts(ctx context.Context, fundingAccount signature.Signer) error { + // Allow the runtime to withdraw some funds from the funding account. + rtAddress := staking.NewRuntimeAddress(r.runtimeID) + + tx := staking.NewAllowTx(0, nil, &staking.Allow{ + Beneficiary: rtAddress, + AmountChange: *quantity.NewFromUint64(100000), + }) + if err := r.FundSignAndSubmitTx(ctx, fundingAccount, tx); err != nil { + r.Logger.Error("failed to sign and submit allow transaction", + "tx", tx, + "signer", fundingAccount.Public(), + ) + return fmt.Errorf("failed to sign and submit allow tx: %w", err) + } + + // Configure the address used for runtime tests. + r.testAddress = staking.NewAddress(fundingAccount.Public()) + + // Query initial account balance. + acct, err := r.Consensus().Staking().Account(ctx, &staking.OwnerQuery{ + Height: consensus.HeightLatest, + Owner: r.testAddress, + }) + if err != nil { + return fmt.Errorf("failed to query account: %w", err) + } + r.testBalance = acct.General.Balance + + return nil } // Implements Workload. @@ -341,11 +518,16 @@ func (r *runtime) Run( } r.reckonedKeyValueState = make(map[string]string) + // Initialize staking accounts for testing runtime interactions. + if err = r.initAccounts(ctx, fundingAccount); err != nil { + return fmt.Errorf("failed to initialize accounts: %w", err) + } + // Set up the runtime client. rtc := runtimeClient.NewRuntimeClient(conn) // Wait for 2nd epoch, so that runtimes are up and running. - r.logger.Info("waiting for 2nd epoch") + r.Logger.Info("waiting for 2nd epoch") if err := cnsc.WaitEpoch(ctx, 2); err != nil { return fmt.Errorf("failed waiting for 2nd epoch: %w", err) } @@ -386,6 +568,14 @@ func (r *runtime) Run( if err := r.doMessageRequest(ctx, rng, rtc); err != nil { return fmt.Errorf("doMessageRequest failure: %w", err) } + case runtimeRequestWithdraw: + if err := r.doWithdrawRequest(ctx, rng, rtc); err != nil { + return fmt.Errorf("doWithdrawRequest failure: %w", err) + } + case runtimeRequestTransfer: + if err := r.doTransferRequest(ctx, rng, rtc); err != nil { + return fmt.Errorf("doTransferRequest failure: %w", err) + } default: return fmt.Errorf("unimplemented") } diff --git a/go/oasis-node/cmd/debug/txsource/workload/workload.go b/go/oasis-node/cmd/debug/txsource/workload/workload.go index c6dd65061c1..0f9c7e9f7a2 100644 --- a/go/oasis-node/cmd/debug/txsource/workload/workload.go +++ b/go/oasis-node/cmd/debug/txsource/workload/workload.go @@ -80,6 +80,11 @@ func (bw *BaseWorkload) Init( bw.fundingAccount = fundingAccount } +// Consensus returns the consensus client backend. +func (bw *BaseWorkload) Consensus() consensus.ClientBackend { + return bw.cc +} + // GasPrice returns the configured consensus gas price. func (bw *BaseWorkload) GasPrice() uint64 { // NOTE: This cannot fail as workloads use static price discovery. diff --git a/go/roothash/api/block/message.go b/go/roothash/api/block/message.go index 98435af89fc..018bc8392e2 100644 --- a/go/roothash/api/block/message.go +++ b/go/roothash/api/block/message.go @@ -3,12 +3,15 @@ package block import ( "fmt" + "github.com/oasisprotocol/oasis-core/go/common/cbor" "github.com/oasisprotocol/oasis-core/go/common/crypto/hash" + staking "github.com/oasisprotocol/oasis-core/go/staking/api" ) // Message is a message that can be sent by a runtime. type Message struct { - Noop *NoopMessage `json:"noop,omitempty"` + Noop *NoopMessage `json:"noop,omitempty"` + Staking *StakingMessage `json:"staking,omitempty"` } // ValidateBasic performs basic validation of the runtime message. @@ -16,6 +19,8 @@ func (m *Message) ValidateBasic() error { switch { case m.Noop != nil: return m.Noop.ValidateBasic() + case m.Staking != nil: + return m.Staking.ValidateBasic() default: return fmt.Errorf("runtime message has no fields set") } @@ -41,3 +46,25 @@ type NoopMessage struct { func (nm *NoopMessage) ValidateBasic() error { return nil } + +// StakingMessage is a runtime message that allows a runtime to perform staking operations. +type StakingMessage struct { + cbor.Versioned + + Transfer *staking.Transfer `json:"transfer,omitempty"` + Withdraw *staking.Withdraw `json:"withdraw,omitempty"` +} + +// ValidateBasic performs basic validation of the runtime message. +func (sm *StakingMessage) ValidateBasic() error { + switch { + case sm.Transfer != nil: + // No validation at this time. + return nil + case sm.Withdraw != nil: + // No validation at this time. + return nil + default: + return fmt.Errorf("staking runtime message has no fields set") + } +} diff --git a/go/roothash/api/block/message_test.go b/go/roothash/api/block/message_test.go index 3a863f4501b..bc4cae9508f 100644 --- a/go/roothash/api/block/message_test.go +++ b/go/roothash/api/block/message_test.go @@ -6,6 +6,7 @@ import ( "github.com/stretchr/testify/require" "github.com/oasisprotocol/oasis-core/go/common/crypto/hash" + staking "github.com/oasisprotocol/oasis-core/go/staking/api" ) func TestMessageHash(t *testing.T) { @@ -18,6 +19,8 @@ func TestMessageHash(t *testing.T) { {nil, "c672b8d1ef56ed28ab87c3622c5114069bdd3ad7b8f9737498d0c01ecef0967a"}, {[]Message{}, "c672b8d1ef56ed28ab87c3622c5114069bdd3ad7b8f9737498d0c01ecef0967a"}, {[]Message{{Noop: &NoopMessage{}}}, "c8b55f87109e30fe2ba57507ffc0e96e40df7c0d24dfef82a858632f5f8420f1"}, + {[]Message{{Staking: &StakingMessage{Transfer: &staking.Transfer{}}}}, "a6b91f974b34a9192efd12025659a768520d2f04e1dae9839677456412cdb2be"}, + {[]Message{{Staking: &StakingMessage{Withdraw: &staking.Withdraw{}}}}, "069b0fda76d804e3fd65d4bbd875c646f15798fb573ac613100df67f5ba4c3fd"}, } { var h hash.Hash err := h.UnmarshalHex(tc.expectedHash) @@ -37,6 +40,8 @@ func TestMessageValidateBasic(t *testing.T) { }{ {"NoFieldsSet", Message{}, false}, {"ValidNoop", Message{Noop: &NoopMessage{}}, true}, + {"StakingNoFieldsSet", Message{Staking: &StakingMessage{}}, false}, + {"ValidStaking", Message{Staking: &StakingMessage{Transfer: &staking.Transfer{}}}, true}, } { err := tc.msg.ValidateBasic() if tc.valid { diff --git a/runtime/src/common/mod.rs b/runtime/src/common/mod.rs index 14a0f1cdf3f..25f689f9f00 100644 --- a/runtime/src/common/mod.rs +++ b/runtime/src/common/mod.rs @@ -12,5 +12,6 @@ pub mod registry; pub mod roothash; pub mod runtime; pub mod sgx; +pub mod staking; pub mod time; pub mod version; diff --git a/runtime/src/common/roothash.rs b/runtime/src/common/roothash.rs index ce42f2752e3..6713476f0f2 100644 --- a/runtime/src/common/roothash.rs +++ b/runtime/src/common/roothash.rs @@ -1,4 +1,4 @@ -//! Roothash structures. +//! Consensus roothash structures. //! //! # Note //! @@ -10,6 +10,7 @@ use serde_repr::*; use super::{ cbor, crypto::{hash::Hash, signature::SignatureBundle}, + staking, }; /// Runtime block. @@ -56,6 +57,13 @@ impl Default for HeaderType { pub enum Message { #[serde(rename = "noop")] Noop {}, + + #[serde(rename = "staking")] + Staking { + v: u16, + #[serde(flatten)] + msg: StakingMessage, + }, } impl Message { @@ -69,6 +77,14 @@ impl Message { } } +#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub enum StakingMessage { + #[serde(rename = "transfer")] + Transfer(staking::Transfer), + #[serde(rename = "withdraw")] + Withdraw(staking::Withdraw), +} + /// Result of a message being processed by the consensus layer. #[derive(Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct MessageEvent { @@ -216,6 +232,20 @@ mod tests { vec![Message::Noop {}], "c8b55f87109e30fe2ba57507ffc0e96e40df7c0d24dfef82a858632f5f8420f1", ), + ( + vec![Message::Staking { + v: 0, + msg: StakingMessage::Transfer(staking::Transfer::default()), + }], + "a6b91f974b34a9192efd12025659a768520d2f04e1dae9839677456412cdb2be", + ), + ( + vec![Message::Staking { + v: 0, + msg: StakingMessage::Withdraw(staking::Withdraw::default()), + }], + "069b0fda76d804e3fd65d4bbd875c646f15798fb573ac613100df67f5ba4c3fd", + ), ]; for (msgs, expected_hash) in tcs { assert_eq!(Message::messages_hash(&msgs), Hash::from(expected_hash)); diff --git a/runtime/src/common/staking.rs b/runtime/src/common/staking.rs new file mode 100644 index 00000000000..fe02d8e6e5d --- /dev/null +++ b/runtime/src/common/staking.rs @@ -0,0 +1,23 @@ +//! Consensus staking structures. +//! +//! # Note +//! +//! This **MUST** be kept in sync with go/staking/api. +//! +use serde::{Deserialize, Serialize}; + +use super::{address::Address, quantity::Quantity}; + +/// A stake transfer. +#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct Transfer { + pub to: Address, + pub amount: Quantity, +} + +/// A withdrawal from an account. +#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct Withdraw { + pub from: Address, + pub amount: Quantity, +} diff --git a/tests/clients/simple-keyvalue-enc/src/main.rs b/tests/clients/simple-keyvalue-enc/src/main.rs index e56d5b8dcd6..946834406d6 100644 --- a/tests/clients/simple-keyvalue-enc/src/main.rs +++ b/tests/clients/simple-keyvalue-enc/src/main.rs @@ -9,7 +9,7 @@ use tokio::runtime::Runtime; use oasis_core_client::{create_txn_api_client, Node, TxnClient}; use oasis_core_keymanager_client::{self, KeyManagerClient, KeyPairId}; use oasis_core_runtime::common::{crypto::hash::Hash, runtime::RuntimeId}; -use simple_keyvalue_api::{with_api, Key, KeyValue}; +use simple_keyvalue_api::{with_api, Key, KeyValue, Transfer, Withdraw}; with_api! { create_txn_api_client!(SimpleKeyValueClient, api); diff --git a/tests/clients/simple-keyvalue-ops/src/main.rs b/tests/clients/simple-keyvalue-ops/src/main.rs index fabcbefc4a5..7d0514419c1 100644 --- a/tests/clients/simple-keyvalue-ops/src/main.rs +++ b/tests/clients/simple-keyvalue-ops/src/main.rs @@ -7,7 +7,7 @@ use tokio::runtime::Runtime; use oasis_core_client::{create_txn_api_client, Node, TxnClient}; use oasis_core_runtime::common::{crypto::hash::Hash, runtime::RuntimeId}; -use simple_keyvalue_api::{with_api, Key, KeyValue}; +use simple_keyvalue_api::{with_api, Key, KeyValue, Transfer, Withdraw}; with_api! { create_txn_api_client!(KeyValueOpsClient, api); diff --git a/tests/clients/simple-keyvalue/src/main.rs b/tests/clients/simple-keyvalue/src/main.rs index 6a40b6c2200..fb4feb32b81 100644 --- a/tests/clients/simple-keyvalue/src/main.rs +++ b/tests/clients/simple-keyvalue/src/main.rs @@ -25,7 +25,7 @@ use oasis_core_runtime::{ common::{crypto::hash::Hash, runtime::RuntimeId}, storage::MKVS, }; -use simple_keyvalue_api::{with_api, Key, KeyValue}; +use simple_keyvalue_api::{with_api, Key, KeyValue, Transfer, Withdraw}; with_api! { create_txn_api_client!(SimpleKeyValueClient, api); diff --git a/tests/clients/test-long-term/src/main.rs b/tests/clients/test-long-term/src/main.rs index 8ecea9187be..ea5c8a9b6eb 100644 --- a/tests/clients/test-long-term/src/main.rs +++ b/tests/clients/test-long-term/src/main.rs @@ -7,7 +7,7 @@ use tokio::runtime::Runtime; use oasis_core_client::{create_txn_api_client, Node, TxnClient}; use oasis_core_runtime::common::{crypto::hash::Hash, runtime::RuntimeId}; -use simple_keyvalue_api::{with_api, Key, KeyValue}; +use simple_keyvalue_api::{with_api, Key, KeyValue, Transfer, Withdraw}; with_api! { create_txn_api_client!(SimpleKeyValueClient, api); diff --git a/tests/runtimes/simple-keyvalue/api/src/api.rs b/tests/runtimes/simple-keyvalue/api/src/api.rs index 113bc47cd4f..2a001c51c19 100644 --- a/tests/runtimes/simple-keyvalue/api/src/api.rs +++ b/tests/runtimes/simple-keyvalue/api/src/api.rs @@ -1,6 +1,6 @@ use serde::{Deserialize, Serialize}; -use oasis_core_runtime::runtime_api; +use oasis_core_runtime::{common::staking, runtime_api}; #[derive(Clone, Serialize, Deserialize)] pub struct Key { @@ -19,6 +19,18 @@ pub struct KeyValue { pub nonce: Option, } +#[derive(Clone, Serialize, Deserialize)] +pub struct Withdraw { + pub nonce: u64, + pub withdraw: staking::Withdraw, +} + +#[derive(Clone, Serialize, Deserialize)] +pub struct Transfer { + pub nonce: u64, + pub transfer: staking::Transfer, +} + runtime_api! { // Gets runtime ID of the runtime. pub fn get_runtime_id(()) -> Option; @@ -26,6 +38,12 @@ runtime_api! { // Emit a runtime message. pub fn message(u64) -> (); + // Withdraw from the consensus layer into the runtime account. + pub fn consensus_withdraw(Withdraw) -> (); + + // Transfer from the runtime account to another account in the consensus layer. + pub fn consensus_transfer(Transfer) -> (); + // Inserts key and corresponding value and returns old value, if any. // Both parameters are passed using a single serializable struct KeyValue. pub fn insert(KeyValue) -> Option; diff --git a/tests/runtimes/simple-keyvalue/api/src/lib.rs b/tests/runtimes/simple-keyvalue/api/src/lib.rs index 9650428d9bb..ddba0b5972e 100644 --- a/tests/runtimes/simple-keyvalue/api/src/lib.rs +++ b/tests/runtimes/simple-keyvalue/api/src/lib.rs @@ -5,4 +5,4 @@ extern crate oasis_core_runtime; #[macro_use] mod api; -pub use api::{Key, KeyValue}; +pub use api::{Key, KeyValue, Transfer, Withdraw}; diff --git a/tests/runtimes/simple-keyvalue/src/main.rs b/tests/runtimes/simple-keyvalue/src/main.rs index 341a76c79dc..9e81a19f1b6 100644 --- a/tests/runtimes/simple-keyvalue/src/main.rs +++ b/tests/runtimes/simple-keyvalue/src/main.rs @@ -12,7 +12,7 @@ use oasis_core_runtime::{ mrae::deoxysii::{DeoxysII, KEY_SIZE, NONCE_SIZE, TAG_SIZE}, }, key_format::KeyFormat, - roothash::Message, + roothash::{Message, StakingMessage}, runtime::RuntimeId, version::Version, }, @@ -27,7 +27,7 @@ use oasis_core_runtime::{ version_from_cargo, Protocol, RpcDemux, RpcDispatcher, TxnDispatcher, TxnMethDispatcher, }; use simple_keymanager::trusted_policy_signers; -use simple_keyvalue_api::{with_api, Key, KeyValue}; +use simple_keyvalue_api::{with_api, Key, KeyValue, Transfer, Withdraw}; /// Key format used for transaction artifacts. #[derive(Debug)] @@ -70,7 +70,7 @@ fn get_runtime_id(_args: &(), ctx: &mut TxnContext) -> Result> { Ok(Some(rctx.test_runtime_id.to_string())) } -// Emit a message and schedule to check its result in the next round. +/// Emit a message and schedule to check its result in the next round. fn message(_args: &u64, ctx: &mut TxnContext) -> Result<()> { if ctx.check_only { return Err(CheckOnlySuccess::default().into()); @@ -93,6 +93,50 @@ fn message(_args: &u64, ctx: &mut TxnContext) -> Result<()> { Ok(()) } +/// Withdraw from the consensus layer into the runtime account. +fn consensus_withdraw(args: &Withdraw, ctx: &mut TxnContext) -> Result<()> { + if ctx.check_only { + return Err(CheckOnlySuccess::default().into()); + } + + StorageContext::with_current(|mkvs, _untrusted_local| { + let index = ctx.emit_message(Message::Staking { + v: 0, + msg: StakingMessage::Withdraw(args.withdraw.clone()), + }); + + mkvs.insert( + IoContext::create_child(&ctx.io_ctx), + &PendingMessagesKeyFormat { index }.encode(), + b"withdraw", + ); + }); + + Ok(()) +} + +/// Transfer from the runtime account to another account in the consensus layer. +fn consensus_transfer(args: &Transfer, ctx: &mut TxnContext) -> Result<()> { + if ctx.check_only { + return Err(CheckOnlySuccess::default().into()); + } + + StorageContext::with_current(|mkvs, _untrusted_local| { + let index = ctx.emit_message(Message::Staking { + v: 0, + msg: StakingMessage::Transfer(args.transfer.clone()), + }); + + mkvs.insert( + IoContext::create_child(&ctx.io_ctx), + &PendingMessagesKeyFormat { index }.encode(), + b"transfer", + ); + }); + + Ok(()) +} + /// Insert a key/value pair. fn insert(args: &KeyValue, ctx: &mut TxnContext) -> Result> { if args.value.as_bytes().len() > 128 { @@ -325,17 +369,25 @@ impl BlockHandler { }); // Make sure metadata is as expected. - assert_eq!( - meta, - Some(b"noop".to_vec()), - "message metadata must be correct" - ); - - // Make sure the message was successfully processed. - assert!( - ev.is_success(), - "messages should have been successfully processed" - ); + match meta.as_ref().map(|v| v.as_slice()) { + Some(b"noop") => { + // Make sure the message was successfully processed. + assert!( + ev.is_success(), + "messages should have been successfully processed" + ); + } + + Some(b"withdraw") => { + // Withdraw. + } + + Some(b"transfer") => { + // Transfer. + } + + meta => panic!("unexpected message metadata: {:?}", meta), + } } // Check if there are any leftover pending messages metadata.