diff --git a/.changelog/4922.feature.md b/.changelog/4922.feature.md new file mode 100644 index 00000000000..5fb9d6a5c43 --- /dev/null +++ b/.changelog/4922.feature.md @@ -0,0 +1 @@ +runtime: Add client node TEE freshness verification diff --git a/go/consensus/api/api.go b/go/consensus/api/api.go index 48e0fff4d1b..43c1aa69dc5 100644 --- a/go/consensus/api/api.go +++ b/go/consensus/api/api.go @@ -155,6 +155,10 @@ type ClientBackend interface { // in a block. Use SubmitTxNoWait if you only need to broadcast the transaction. SubmitTx(ctx context.Context, tx *transaction.SignedTransaction) error + // SubmitTxWithProof submits a signed consensus transaction, waits for the transaction to be + // included in a block and returns a proof of inclusion. + SubmitTxWithProof(ctx context.Context, tx *transaction.SignedTransaction) (*transaction.Proof, error) + // StateToGenesis returns the genesis state at the specified block height. StateToGenesis(ctx context.Context, height int64) (*genesis.Document, error) diff --git a/go/consensus/api/grpc.go b/go/consensus/api/grpc.go index 8c1dcf795aa..893c8c89da0 100644 --- a/go/consensus/api/grpc.go +++ b/go/consensus/api/grpc.go @@ -26,6 +26,8 @@ var ( // methodSubmitTx is the SubmitTx method. methodSubmitTx = serviceName.NewMethod("SubmitTx", transaction.SignedTransaction{}) + // methodSubmitTxWithProof is the SubmitTxWithProof method. + methodSubmitTxWithProof = serviceName.NewMethod("SubmitTxWithProof", transaction.SignedTransaction{}) // methodStateToGenesis is the StateToGenesis method. methodStateToGenesis = serviceName.NewMethod("StateToGenesis", int64(0)) // methodEstimateGas is the EstimateGas method. @@ -78,6 +80,10 @@ var ( MethodName: methodSubmitTx.ShortName(), Handler: handlerSubmitTx, }, + { + MethodName: methodSubmitTxWithProof.ShortName(), + Handler: handlerSubmitTxWithProof, + }, { MethodName: methodStateToGenesis.ShortName(), Handler: handlerStateToGenesis, @@ -196,6 +202,29 @@ func handlerSubmitTx( return interceptor(ctx, rq, info, handler) } +func handlerSubmitTxWithProof( + srv interface{}, + ctx context.Context, + dec func(interface{}) error, + interceptor grpc.UnaryServerInterceptor, +) (interface{}, error) { + rq := new(transaction.SignedTransaction) + if err := dec(rq); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ClientBackend).SubmitTxWithProof(ctx, rq) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: methodSubmitTxWithProof.FullName(), + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ClientBackend).SubmitTxWithProof(ctx, req.(*transaction.SignedTransaction)) + } + return interceptor(ctx, rq, info, handler) +} + func handlerStateToGenesis( srv interface{}, ctx context.Context, @@ -739,6 +768,14 @@ func (c *consensusClient) SubmitTx(ctx context.Context, tx *transaction.SignedTr return c.conn.Invoke(ctx, methodSubmitTx.FullName(), tx, nil) } +func (c *consensusClient) SubmitTxWithProof(ctx context.Context, tx *transaction.SignedTransaction) (*transaction.Proof, error) { + var proof transaction.Proof + if err := c.conn.Invoke(ctx, methodSubmitTxWithProof.FullName(), tx, &proof); err != nil { + return nil, err + } + return &proof, nil +} + func (c *consensusClient) StateToGenesis(ctx context.Context, height int64) (*genesis.Document, error) { var rsp genesis.Document if err := c.conn.Invoke(ctx, methodStateToGenesis.FullName(), height, &rsp); err != nil { diff --git a/go/consensus/api/submission.go b/go/consensus/api/submission.go index 16831b5ceb6..b8203d451a2 100644 --- a/go/consensus/api/submission.go +++ b/go/consensus/api/submission.go @@ -64,6 +64,13 @@ type SubmissionManager interface { // // It also automatically handles retries in case the nonce was incorrectly estimated. SignAndSubmitTx(ctx context.Context, signer signature.Signer, tx *transaction.Transaction) error + + // SignAndSubmitTxWithProof populates the nonce and fee fields in the transaction, signs + // the transaction with the passed signer, submits it to consensus backend and creates + // a proof of inclusion. + // + // It also automatically handles retries in case the nonce was incorrectly estimated. + SignAndSubmitTxWithProof(ctx context.Context, signer signature.Signer, tx *transaction.Transaction) (*transaction.SignedTransaction, *transaction.Proof, error) } type submissionManager struct { @@ -124,7 +131,7 @@ func (m *submissionManager) EstimateGasAndSetFee(ctx context.Context, signer sig return nil } -func (m *submissionManager) signAndSubmitTx(ctx context.Context, signer signature.Signer, tx *transaction.Transaction) error { +func (m *submissionManager) signAndSubmitTx(ctx context.Context, signer signature.Signer, tx *transaction.Transaction, withProof bool) (*transaction.SignedTransaction, *transaction.Proof, error) { // Update transaction nonce. var err error signerAddr := staking.NewAddress(signer.Public()) @@ -134,14 +141,14 @@ func (m *submissionManager) signAndSubmitTx(ctx context.Context, signer signatur if errors.Is(err, ErrNoCommittedBlocks) { // No committed blocks available, retry submission. m.logger.Debug("retrying transaction submission due to no committed blocks") - return err + return nil, nil, err } - return backoff.Permanent(err) + return nil, nil, backoff.Permanent(err) } // Estimate the fee. if err = m.EstimateGasAndSetFee(ctx, signer, tx); err != nil { - return fmt.Errorf("failed to estimate fee: %w", err) + return nil, nil, fmt.Errorf("failed to estimate fee: %w", err) } // Sign the transaction. @@ -150,39 +157,68 @@ func (m *submissionManager) signAndSubmitTx(ctx context.Context, signer signatur m.logger.Error("failed to sign transaction", "err", err, ) - return backoff.Permanent(err) + return nil, nil, backoff.Permanent(err) } - if err = m.backend.SubmitTx(ctx, sigTx); err != nil { + var proof *transaction.Proof + if withProof { + proof, err = m.backend.SubmitTxWithProof(ctx, sigTx) + } else { + err = m.backend.SubmitTx(ctx, sigTx) + } + if err != nil { switch { case errors.Is(err, transaction.ErrUpgradePending): // Pending upgrade, retry submission. m.logger.Debug("retrying transaction submission due to pending upgrade") - return err + return nil, nil, err case errors.Is(err, transaction.ErrInvalidNonce): // Invalid nonce, retry submission. m.logger.Debug("retrying transaction submission due to invalid nonce", "account_address", signerAddr, "nonce", tx.Nonce, ) - return err + return nil, nil, err default: - return backoff.Permanent(err) + return nil, nil, backoff.Permanent(err) } } - return nil + return sigTx, proof, nil } -// Implements SubmissionManager. -func (m *submissionManager) SignAndSubmitTx(ctx context.Context, signer signature.Signer, tx *transaction.Transaction) error { +func (m *submissionManager) signAndSubmitTxWithRetry(ctx context.Context, signer signature.Signer, tx *transaction.Transaction, withProof bool) (*transaction.SignedTransaction, *transaction.Proof, error) { sched := cmnBackoff.NewExponentialBackOff() sched.MaxInterval = maxSubmissionRetryInterval sched.MaxElapsedTime = maxSubmissionRetryElapsedTime - return backoff.Retry(func() error { - return m.signAndSubmitTx(ctx, signer, tx) - }, backoff.WithContext(sched, ctx)) + var ( + sigTx *transaction.SignedTransaction + proof *transaction.Proof + ) + + f := func() error { + var err error + sigTx, proof, err = m.signAndSubmitTx(ctx, signer, tx, withProof) + return err + } + + if err := backoff.Retry(f, backoff.WithContext(sched, ctx)); err != nil { + return nil, nil, err + } + + return sigTx, proof, nil +} + +// Implements SubmissionManager. +func (m *submissionManager) SignAndSubmitTx(ctx context.Context, signer signature.Signer, tx *transaction.Transaction) error { + _, _, err := m.signAndSubmitTxWithRetry(ctx, signer, tx, false) + return err +} + +// Implements SubmissionManager. +func (m *submissionManager) SignAndSubmitTxWithProof(ctx context.Context, signer signature.Signer, tx *transaction.Transaction) (*transaction.SignedTransaction, *transaction.Proof, error) { + return m.signAndSubmitTxWithRetry(ctx, signer, tx, true) } // NewSubmissionManager creates a new transaction submission manager. @@ -209,6 +245,18 @@ func SignAndSubmitTx(ctx context.Context, backend Backend, signer signature.Sign return backend.SubmissionManager().SignAndSubmitTx(ctx, signer, tx) } +// SignAndSubmitTxWithProof is a helper function that signs and submits +// a transaction to the consensus backend and creates a proof of inclusion. +// +// If the nonce is set to zero, it will be automatically filled in based on the +// current consensus state. +// +// If the fee is set to nil, it will be automatically filled in based on gas +// estimation and current gas price discovery. +func SignAndSubmitTxWithProof(ctx context.Context, backend Backend, signer signature.Signer, tx *transaction.Transaction) (*transaction.SignedTransaction, *transaction.Proof, error) { + return backend.SubmissionManager().SignAndSubmitTxWithProof(ctx, signer, tx) +} + // NoOpSubmissionManager implements a submission manager that doesn't support submitting transactions. type NoOpSubmissionManager struct{} @@ -226,3 +274,8 @@ func (m *NoOpSubmissionManager) EstimateGasAndSetFee(ctx context.Context, signer func (m *NoOpSubmissionManager) SignAndSubmitTx(ctx context.Context, signer signature.Signer, tx *transaction.Transaction) error { return transaction.ErrMethodNotSupported } + +// SignAndSubmitTxWithProof implements SubmissionManager. +func (m *NoOpSubmissionManager) SignAndSubmitTxWithProof(ctx context.Context, signer signature.Signer, tx *transaction.Transaction) (*transaction.SignedTransaction, *transaction.Proof, error) { + return nil, nil, transaction.ErrMethodNotSupported +} diff --git a/go/consensus/api/transaction/transaction.go b/go/consensus/api/transaction/transaction.go index e136f95ee44..f5e291069dd 100644 --- a/go/consensus/api/transaction/transaction.go +++ b/go/consensus/api/transaction/transaction.go @@ -164,7 +164,7 @@ type PrettyTransaction struct { Body interface{} `json:"body,omitempty"` } -// SignedTransaction is a signed transaction. +// SignedTransaction is a signed consensus transaction. type SignedTransaction struct { signature.Signed } @@ -345,3 +345,12 @@ func NewMethodName(module, method string, bodyType interface{}) MethodName { return MethodName(name) } + +// Proof is a proof of transaction inclusion in a block. +type Proof struct { + // Height is the block height at which the transaction was published. + Height int64 `json:"height"` + + // RawProof is the actual raw proof. + RawProof []byte `json:"raw_proof"` +} diff --git a/go/consensus/tendermint/full/common.go b/go/consensus/tendermint/full/common.go index f641078dc46..1ce4b60b787 100644 --- a/go/consensus/tendermint/full/common.go +++ b/go/consensus/tendermint/full/common.go @@ -763,6 +763,11 @@ func (n *commonNode) SubmitTx(ctx context.Context, tx *transaction.SignedTransac return consensusAPI.ErrUnsupported } +// Implements consensusAPI.Backend. +func (n *commonNode) SubmitTxWithProof(ctx context.Context, tx *transaction.SignedTransaction) (*transaction.Proof, error) { + return nil, consensusAPI.ErrUnsupported +} + // Implements consensusAPI.Backend. func (n *commonNode) GetUnconfirmedTransactions(ctx context.Context) ([][]byte, error) { return nil, consensusAPI.ErrUnsupported diff --git a/go/consensus/tendermint/full/full.go b/go/consensus/tendermint/full/full.go index 126cbf4304e..990aba2768e 100644 --- a/go/consensus/tendermint/full/full.go +++ b/go/consensus/tendermint/full/full.go @@ -4,6 +4,7 @@ package full import ( "bytes" "context" + "crypto/sha256" "fmt" "math/rand" "path/filepath" @@ -17,6 +18,7 @@ import ( "github.com/spf13/viper" tmabcitypes "github.com/tendermint/tendermint/abci/types" tmconfig "github.com/tendermint/tendermint/config" + tmmerkle "github.com/tendermint/tendermint/crypto/merkle" tmpubsub "github.com/tendermint/tendermint/libs/pubsub" tmlight "github.com/tendermint/tendermint/light" tmmempool "github.com/tendermint/tendermint/mempool" @@ -244,18 +246,56 @@ func (t *fullService) Mode() consensusAPI.Mode { // Implements consensusAPI.Backend. func (t *fullService) SubmitTx(ctx context.Context, tx *transaction.SignedTransaction) error { + if _, err := t.submitTx(ctx, tx); err != nil { + return err + } + return nil +} + +// Implements consensusAPI.Backend. +func (t *fullService) SubmitTxWithProof(ctx context.Context, tx *transaction.SignedTransaction) (*transaction.Proof, error) { + data, err := t.submitTx(ctx, tx) + if err != nil { + return nil, err + } + + txs, err := t.GetTransactions(ctx, data.Height) + if err != nil { + return nil, err + } + + if data.Index >= uint32(len(txs)) { + return nil, fmt.Errorf("tendermint: invalid transaction index") + } + + // Tendermint Merkle tree is computed over hashes and not over transactions. + hashes := make([][]byte, 0, len(txs)) + for _, tx := range txs { + hash := sha256.Sum256(tx) + hashes = append(hashes, hash[:]) + } + + _, proofs := tmmerkle.ProofsFromByteSlices(hashes) + + return &transaction.Proof{ + Height: data.Height, + RawProof: cbor.Marshal(proofs[data.Index]), + }, nil +} + +func (t *fullService) submitTx(ctx context.Context, tx *transaction.SignedTransaction) (*tmtypes.EventDataTx, error) { // Subscribe to the transaction being included in a block. data := cbor.Marshal(tx) query := tmtypes.EventQueryTxFor(data) subID := t.newSubscriberID() txSub, err := t.subscribe(subID, query) if err != nil { - return err + return nil, err } if ptrSub, ok := txSub.(*tendermintPubsubBuffer).tmSubscription.(*tmpubsub.Subscription); ok && ptrSub == nil { t.Logger.Debug("broadcastTx: service has shut down. Cancel our context to recover") <-ctx.Done() - return ctx.Err() + return nil, ctx.Err() } defer t.unsubscribe(subID, query) // nolint: errcheck @@ -265,28 +305,29 @@ func (t *fullService) SubmitTx(ctx context.Context, tx *transaction.SignedTransa recheckCh, recheckSub, err := t.mux.WatchInvalidatedTx(txHash) if err != nil { - return err + return nil, err } defer recheckSub.Close() // First try to broadcast. if err := t.broadcastTxRaw(data); err != nil { - return err + return nil, err } // Wait for the transaction to be included in a block. select { case v := <-recheckCh: - return v + return nil, v case v := <-txSub.Out(): - if result := v.Data().(tmtypes.EventDataTx).Result; !result.IsOK() { - return errors.FromCode(result.GetCodespace(), result.GetCode(), result.GetLog()) + data := v.Data().(tmtypes.EventDataTx) + if result := data.Result; !result.IsOK() { + return nil, errors.FromCode(result.GetCodespace(), result.GetCode(), result.GetLog()) } - return nil + return &data, nil case <-txSub.Cancelled(): - return context.Canceled + return nil, context.Canceled case <-ctx.Done(): - return ctx.Err() + return nil, ctx.Err() } } diff --git a/go/consensus/tendermint/seed/seed.go b/go/consensus/tendermint/seed/seed.go index 5aef4e55618..cca6a9fc6a3 100644 --- a/go/consensus/tendermint/seed/seed.go +++ b/go/consensus/tendermint/seed/seed.go @@ -201,6 +201,11 @@ func (srv *seedService) SubmitTx(ctx context.Context, tx *transaction.SignedTran return consensus.ErrUnsupported } +// Implements consensus.Backend. +func (srv *seedService) SubmitTxWithProof(ctx context.Context, tx *transaction.SignedTransaction) (*transaction.Proof, error) { + return nil, consensus.ErrUnsupported +} + // Implements consensus.Backend. func (srv *seedService) StateToGenesis(ctx context.Context, height int64) (*genesis.Document, error) { return nil, consensus.ErrUnsupported diff --git a/go/runtime/host/protocol/types.go b/go/runtime/host/protocol/types.go index db8de15208d..e1f04d07455 100644 --- a/go/runtime/host/protocol/types.go +++ b/go/runtime/host/protocol/types.go @@ -14,6 +14,7 @@ import ( "github.com/oasisprotocol/oasis-core/go/common/sgx/quote" "github.com/oasisprotocol/oasis-core/go/common/version" consensus "github.com/oasisprotocol/oasis-core/go/consensus/api" + consensusTx "github.com/oasisprotocol/oasis-core/go/consensus/api/transaction" consensusResults "github.com/oasisprotocol/oasis-core/go/consensus/api/transaction/results" roothash "github.com/oasisprotocol/oasis-core/go/roothash/api" "github.com/oasisprotocol/oasis-core/go/roothash/api/block" @@ -112,6 +113,8 @@ type Body struct { HostFetchTxBatchResponse *HostFetchTxBatchResponse `json:",omitempty"` HostFetchGenesisHeightRequest *HostFetchGenesisHeightRequest `json:",omitempty"` HostFetchGenesisHeightResponse *HostFetchGenesisHeightResponse `json:",omitempty"` + HostProveFreshnessRequest *HostProveFreshnessRequest `json:",omitempty"` + HostProveFreshnessResponse *HostProveFreshnessResponse `json:",omitempty"` } // Type returns the message type by determining the name of the first non-nil member. @@ -530,3 +533,16 @@ type HostFetchTxBatchResponse struct { // Batch is a batch of transactions. Batch [][]byte `json:"batch,omitempty"` } + +// HostProveFreshnessRequest is a request to host to prove state freshness. +type HostProveFreshnessRequest struct { + Blob [32]byte `json:"blob"` +} + +// HostProveFreshnessResponse is a response from host proving state freshness. +type HostProveFreshnessResponse struct { + // SignedTx is a signed prove freshness transaction. + SignedTx *consensusTx.SignedTransaction `json:"signed_tx"` + // Proof of transaction inclusion in a block. + Proof *consensusTx.Proof `json:"proof"` +} diff --git a/go/runtime/host/sandbox/sandbox.go b/go/runtime/host/sandbox/sandbox.go index 059af0742e7..7f588a04e14 100644 --- a/go/runtime/host/sandbox/sandbox.go +++ b/go/runtime/host/sandbox/sandbox.go @@ -28,6 +28,7 @@ const ( runtimeInitTimeout = 1 * time.Second runtimeExtendedInitTimeout = 120 * time.Second runtimeInterruptTimeout = 1 * time.Second + resetTickerTimeout = 15 * time.Minute bindHostSocketPath = "/host.sock" @@ -418,13 +419,7 @@ func (r *sandboxedRuntime) handleAbortRequest(rq *abortRequest) error { } func (r *sandboxedRuntime) manager() { - // Initialize a ticker channel for restarting the process. Initialize it with a closed channel - // so that the first time, the process will be restarted immediately. var ticker *backoff.Ticker - var tickerCh <-chan time.Time - ch := make(chan time.Time) - tickerCh = ch - close(ch) defer func() { r.logger.Warn("terminating runtime") @@ -454,41 +449,42 @@ func (r *sandboxedRuntime) manager() { for { // Make sure to restart the process if terminated. if r.process == nil { + firstTickCh := make(chan struct{}, 1) + if ticker == nil { + // Initialize a ticker for restarting the process. We use a separate channel + // to restart the process immediately on the first run, as we don't want to wait + // for the first tick. + ticker = backoff.NewTicker(cmnBackoff.NewExponentialBackOff()) + firstTickCh <- struct{}{} + attempt = 0 + } + select { case <-r.stopCh: r.logger.Warn("termination requested") return - case <-tickerCh: - attempt++ - r.logger.Info("starting runtime", - "attempt", attempt, + case <-firstTickCh: + case <-ticker.C: + } + + attempt++ + r.logger.Info("starting runtime", + "attempt", attempt, + ) + + if err := r.startProcess(); err != nil { + r.logger.Error("failed to start runtime", + "err", err, ) - if err := r.startProcess(); err != nil { - r.logger.Error("failed to start runtime", - "err", err, - ) - - // Notify subscribers that a runtime has failed to start. - r.notifier.Broadcast(&host.Event{ - FailedToStart: &host.FailedToStartEvent{ - Error: err, - }, - }) - - if ticker == nil { - ticker = backoff.NewTicker(cmnBackoff.NewExponentialBackOff()) - tickerCh = ticker.C - } - continue - } - - // Runtime started successfully. - if ticker != nil { - ticker.Stop() - ticker = nil - } - attempt = 0 + // Notify subscribers that a runtime has failed to start. + r.notifier.Broadcast(&host.Event{ + FailedToStart: &host.FailedToStartEvent{ + Error: err, + }, + }) + + continue } } @@ -504,7 +500,6 @@ func (r *sandboxedRuntime) manager() { r.logger.Error("received unknown request type", "request_type", fmt.Sprintf("%T", rq), ) - continue } case <-r.stopCh: r.logger.Warn("termination requested") @@ -523,7 +518,13 @@ func (r *sandboxedRuntime) manager() { // Notify subscribers that the runtime has stopped. r.notifier.Broadcast(&host.Event{Stopped: &host.StoppedEvent{}}) - continue + case <-time.After(resetTickerTimeout): + // Reset the ticker if things work smoothly. Otherwise, keep on using the old ticker as + // it can happen that the runtime constantly terminates after a successful start. + if ticker != nil { + ticker.Stop() + ticker = nil + } } } } diff --git a/go/runtime/registry/host.go b/go/runtime/registry/host.go index 163243d8db7..68b22b933a6 100644 --- a/go/runtime/registry/host.go +++ b/go/runtime/registry/host.go @@ -10,6 +10,7 @@ import ( "github.com/eapache/channels" "github.com/oasisprotocol/oasis-core/go/common/cbor" + "github.com/oasisprotocol/oasis-core/go/common/identity" "github.com/oasisprotocol/oasis-core/go/common/logging" "github.com/oasisprotocol/oasis-core/go/common/version" consensus "github.com/oasisprotocol/oasis-core/go/consensus/api" @@ -153,6 +154,9 @@ type RuntimeHostHandlerEnvironment interface { // GetTxPool returns the transaction pool for this runtime. GetTxPool(ctx context.Context) (txpool.TransactionPool, error) + + // GetNodeIdentity returns the identity of a node running this runtime. + GetNodeIdentity(ctx context.Context) (*identity.Identity, error) } // RuntimeHostHandler is a runtime host handler suitable for compute runtimes. It provides the @@ -165,22 +169,22 @@ type runtimeHostHandler struct { func (h *runtimeHostHandler) handleHostRPCCall( ctx context.Context, - request *protocol.HostRPCCallRequest, -) (*protocol.Body, error) { - switch request.Endpoint { + rq *protocol.HostRPCCallRequest, +) (*protocol.HostRPCCallResponse, error) { + switch rq.Endpoint { case runtimeKeymanager.EnclaveRPCEndpoint: // Call into the remote key manager. kmCli, err := h.env.GetKeyManagerClient(ctx) if err != nil { return nil, err } - res, err := kmCli.CallEnclave(ctx, request.Request, request.PeerFeedback) + res, err := kmCli.CallEnclave(ctx, rq.Request, rq.PeerFeedback) if err != nil { return nil, err } - return &protocol.Body{HostRPCCallResponse: &protocol.HostRPCCallResponse{ + return &protocol.HostRPCCallResponse{ Response: cbor.FixSliceForSerde(res), - }}, nil + }, nil default: return nil, errEndpointNotSupported } @@ -188,10 +192,10 @@ func (h *runtimeHostHandler) handleHostRPCCall( func (h *runtimeHostHandler) handleHostStorageSync( ctx context.Context, - request *protocol.HostStorageSyncRequest, -) (*protocol.Body, error) { + rq *protocol.HostStorageSyncRequest, +) (*protocol.HostStorageSyncResponse, error) { var rs syncer.ReadSyncer - switch request.Endpoint { + switch rq.Endpoint { case protocol.HostStorageEndpointRuntime: // Runtime storage. rs = h.runtime.Storage() @@ -205,12 +209,12 @@ func (h *runtimeHostHandler) handleHostStorageSync( var rsp *storage.ProofResponse var err error switch { - case request.SyncGet != nil: - rsp, err = rs.SyncGet(ctx, request.SyncGet) - case request.SyncGetPrefixes != nil: - rsp, err = rs.SyncGetPrefixes(ctx, request.SyncGetPrefixes) - case request.SyncIterate != nil: - rsp, err = rs.SyncIterate(ctx, request.SyncIterate) + case rq.SyncGet != nil: + rsp, err = rs.SyncGet(ctx, rq.SyncGet) + case rq.SyncGetPrefixes != nil: + rsp, err = rs.SyncGetPrefixes(ctx, rq.SyncGetPrefixes) + case rq.SyncIterate != nil: + rsp, err = rs.SyncIterate(ctx, rq.SyncIterate) default: return nil, errMethodNotSupported } @@ -218,51 +222,49 @@ func (h *runtimeHostHandler) handleHostStorageSync( return nil, err } - return &protocol.Body{HostStorageSyncResponse: &protocol.HostStorageSyncResponse{ProofResponse: rsp}}, nil + return &protocol.HostStorageSyncResponse{ProofResponse: rsp}, nil } -func (h *runtimeHostHandler) handleHostLocalStorage( +func (h *runtimeHostHandler) handleHostLocalStorageGet( ctx context.Context, - body *protocol.Body, -) (*protocol.Body, error) { - switch { - case body.HostLocalStorageGetRequest != nil: - value, err := h.runtime.LocalStorage().Get(body.HostLocalStorageGetRequest.Key) - if err != nil { - return nil, err - } - return &protocol.Body{HostLocalStorageGetResponse: &protocol.HostLocalStorageGetResponse{Value: value}}, nil - case body.HostLocalStorageSetRequest != nil: - if err := h.runtime.LocalStorage().Set(body.HostLocalStorageSetRequest.Key, body.HostLocalStorageSetRequest.Value); err != nil { - return nil, err - } - return &protocol.Body{HostLocalStorageSetResponse: &protocol.Empty{}}, nil - default: - return nil, errMethodNotSupported + rq *protocol.HostLocalStorageGetRequest, +) (*protocol.HostLocalStorageGetResponse, error) { + value, err := h.runtime.LocalStorage().Get(rq.Key) + if err != nil { + return nil, err } + return &protocol.HostLocalStorageGetResponse{Value: value}, nil +} + +func (h *runtimeHostHandler) handleHostLocalStorageSet( + ctx context.Context, + rq *protocol.HostLocalStorageSetRequest, +) (*protocol.Empty, error) { + if err := h.runtime.LocalStorage().Set(rq.Key, rq.Value); err != nil { + return nil, err + } + return &protocol.Empty{}, nil } func (h *runtimeHostHandler) handleHostFetchConsensusBlock( ctx context.Context, - request *protocol.HostFetchConsensusBlockRequest, -) (*protocol.Body, error) { - lb, err := h.consensus.GetLightBlock(ctx, int64(request.Height)) + rq *protocol.HostFetchConsensusBlockRequest, +) (*protocol.HostFetchConsensusBlockResponse, error) { + lb, err := h.consensus.GetLightBlock(ctx, int64(rq.Height)) if err != nil { return nil, err } - return &protocol.Body{HostFetchConsensusBlockResponse: &protocol.HostFetchConsensusBlockResponse{ - Block: *lb, - }}, nil + return &protocol.HostFetchConsensusBlockResponse{Block: *lb}, nil } func (h *runtimeHostHandler) handleHostFetchConsensusEvents( ctx context.Context, - request *protocol.HostFetchConsensusEventsRequest, -) (*protocol.Body, error) { + rq *protocol.HostFetchConsensusEventsRequest, +) (*protocol.HostFetchConsensusEventsResponse, error) { var evs []*consensusResults.Event - switch request.Kind { + switch rq.Kind { case protocol.EventKindStaking: - sevs, err := h.consensus.Staking().GetEvents(ctx, int64(request.Height)) + sevs, err := h.consensus.Staking().GetEvents(ctx, int64(rq.Height)) if err != nil { return nil, err } @@ -271,7 +273,7 @@ func (h *runtimeHostHandler) handleHostFetchConsensusEvents( evs = append(evs, &consensusResults.Event{Staking: sev}) } case protocol.EventKindRegistry: - revs, err := h.consensus.Registry().GetEvents(ctx, int64(request.Height)) + revs, err := h.consensus.Registry().GetEvents(ctx, int64(rq.Height)) if err != nil { return nil, err } @@ -280,7 +282,7 @@ func (h *runtimeHostHandler) handleHostFetchConsensusEvents( evs = append(evs, &consensusResults.Event{Registry: rev}) } case protocol.EventKindRootHash: - revs, err := h.consensus.RootHash().GetEvents(ctx, int64(request.Height)) + revs, err := h.consensus.RootHash().GetEvents(ctx, int64(rq.Height)) if err != nil { return nil, err } @@ -289,7 +291,7 @@ func (h *runtimeHostHandler) handleHostFetchConsensusEvents( evs = append(evs, &consensusResults.Event{RootHash: rev}) } case protocol.EventKindGovernance: - gevs, err := h.consensus.Governance().GetEvents(ctx, int64(request.Height)) + gevs, err := h.consensus.Governance().GetEvents(ctx, int64(rq.Height)) if err != nil { return nil, err } @@ -300,71 +302,101 @@ func (h *runtimeHostHandler) handleHostFetchConsensusEvents( default: return nil, errMethodNotSupported } - return &protocol.Body{HostFetchConsensusEventsResponse: &protocol.HostFetchConsensusEventsResponse{ - Events: evs, - }}, nil + return &protocol.HostFetchConsensusEventsResponse{Events: evs}, nil } func (h *runtimeHostHandler) handleHostFetchGenesisHeight( ctx context.Context, - request *protocol.HostFetchGenesisHeightRequest, -) (*protocol.Body, error) { + rq *protocol.HostFetchGenesisHeightRequest, +) (*protocol.HostFetchGenesisHeightResponse, error) { doc, err := h.consensus.GetGenesisDocument(ctx) if err != nil { return nil, err } - return &protocol.Body{HostFetchGenesisHeightResponse: &protocol.HostFetchGenesisHeightResponse{ - Height: uint64(doc.Height), - }}, nil + return &protocol.HostFetchGenesisHeightResponse{Height: uint64(doc.Height)}, nil } func (h *runtimeHostHandler) handleHostFetchTxBatch( ctx context.Context, - request *protocol.HostFetchTxBatchRequest, -) (*protocol.Body, error) { + rq *protocol.HostFetchTxBatchRequest, +) (*protocol.HostFetchTxBatchResponse, error) { txPool, err := h.env.GetTxPool(ctx) if err != nil { return nil, err } - batch := txPool.GetSchedulingExtra(request.Offset, request.Limit) + batch := txPool.GetSchedulingExtra(rq.Offset, rq.Limit) raw := make([][]byte, 0, len(batch)) for _, tx := range batch { raw = append(raw, tx.Raw()) } - return &protocol.Body{HostFetchTxBatchResponse: &protocol.HostFetchTxBatchResponse{ - Batch: raw, - }}, nil + return &protocol.HostFetchTxBatchResponse{Batch: raw}, nil +} + +func (h *runtimeHostHandler) handleHostProveFreshness( + ctx context.Context, + rq *protocol.HostProveFreshnessRequest, +) (*protocol.HostProveFreshnessResponse, error) { + identity, err := h.env.GetNodeIdentity(ctx) + if err != nil { + return nil, err + } + tx := registry.NewProveFreshnessTx(0, nil, rq.Blob) + sigTx, proof, err := consensus.SignAndSubmitTxWithProof(ctx, h.consensus, identity.NodeSigner, tx) + if err != nil { + return nil, err + } + + return &protocol.HostProveFreshnessResponse{ + SignedTx: sigTx, + Proof: proof, + }, nil } // Implements protocol.Handler. -func (h *runtimeHostHandler) Handle(ctx context.Context, body *protocol.Body) (*protocol.Body, error) { +func (h *runtimeHostHandler) Handle(ctx context.Context, rq *protocol.Body) (*protocol.Body, error) { + var ( + rsp protocol.Body + err error + ) + switch { - case body.HostRPCCallRequest != nil: + case rq.HostRPCCallRequest != nil: // RPC. - return h.handleHostRPCCall(ctx, body.HostRPCCallRequest) - case body.HostStorageSyncRequest != nil: - // Storage. - return h.handleHostStorageSync(ctx, body.HostStorageSyncRequest) - case body.HostLocalStorageGetRequest != nil, body.HostLocalStorageSetRequest != nil: - // Local storage. - return h.handleHostLocalStorage(ctx, body) - case body.HostFetchConsensusBlockRequest != nil: + rsp.HostRPCCallResponse, err = h.handleHostRPCCall(ctx, rq.HostRPCCallRequest) + case rq.HostStorageSyncRequest != nil: + // Storage sync. + rsp.HostStorageSyncResponse, err = h.handleHostStorageSync(ctx, rq.HostStorageSyncRequest) + case rq.HostLocalStorageGetRequest != nil: + // Local storage get. + rsp.HostLocalStorageGetResponse, err = h.handleHostLocalStorageGet(ctx, rq.HostLocalStorageGetRequest) + case rq.HostLocalStorageSetRequest != nil: + // Local storage set. + rsp.HostLocalStorageSetResponse, err = h.handleHostLocalStorageSet(ctx, rq.HostLocalStorageSetRequest) + case rq.HostFetchConsensusBlockRequest != nil: // Consensus light client. - return h.handleHostFetchConsensusBlock(ctx, body.HostFetchConsensusBlockRequest) - case body.HostFetchConsensusEventsRequest != nil: + rsp.HostFetchConsensusBlockResponse, err = h.handleHostFetchConsensusBlock(ctx, rq.HostFetchConsensusBlockRequest) + case rq.HostFetchConsensusEventsRequest != nil: // Consensus events. - return h.handleHostFetchConsensusEvents(ctx, body.HostFetchConsensusEventsRequest) - case body.HostFetchGenesisHeightRequest != nil: + rsp.HostFetchConsensusEventsResponse, err = h.handleHostFetchConsensusEvents(ctx, rq.HostFetchConsensusEventsRequest) + case rq.HostFetchGenesisHeightRequest != nil: // Consensus genesis height. - return h.handleHostFetchGenesisHeight(ctx, body.HostFetchGenesisHeightRequest) - case body.HostFetchTxBatchRequest != nil: + rsp.HostFetchGenesisHeightResponse, err = h.handleHostFetchGenesisHeight(ctx, rq.HostFetchGenesisHeightRequest) + case rq.HostFetchTxBatchRequest != nil: // Transaction pool. - return h.handleHostFetchTxBatch(ctx, body.HostFetchTxBatchRequest) + rsp.HostFetchTxBatchResponse, err = h.handleHostFetchTxBatch(ctx, rq.HostFetchTxBatchRequest) + case rq.HostProveFreshnessRequest != nil: + // Prove freshness. + rsp.HostProveFreshnessResponse, err = h.handleHostProveFreshness(ctx, rq.HostProveFreshnessRequest) default: - return nil, errMethodNotSupported + err = errMethodNotSupported + } + + if err != nil { + return nil, err } + return &rsp, nil } // runtimeHostNotifier is a runtime host notifier suitable for compute runtimes. It handles things diff --git a/go/worker/common/committee/runtime_host.go b/go/worker/common/committee/runtime_host.go index 0d96a5da68c..64c270f718c 100644 --- a/go/worker/common/committee/runtime_host.go +++ b/go/worker/common/committee/runtime_host.go @@ -3,6 +3,7 @@ package committee import ( "context" + "github.com/oasisprotocol/oasis-core/go/common/identity" "github.com/oasisprotocol/oasis-core/go/roothash/api/block" "github.com/oasisprotocol/oasis-core/go/runtime/host" "github.com/oasisprotocol/oasis-core/go/runtime/host/protocol" @@ -44,6 +45,11 @@ func (env *nodeEnvironment) GetTxPool(ctx context.Context) (txpool.TransactionPo return env.n.TxPool, nil } +// GetIdentity implements RuntimeHostHandlerEnvironment. +func (env *nodeEnvironment) GetNodeIdentity(ctx context.Context) (*identity.Identity, error) { + return env.n.Identity, nil +} + // NewRuntimeHostHandler implements RuntimeHostHandlerFactory. func (n *Node) NewRuntimeHostHandler() protocol.Handler { return runtimeRegistry.NewRuntimeHostHandler(&nodeEnvironment{n}, n.Runtime, n.Consensus) diff --git a/runtime/src/common/crypto/signature.rs b/runtime/src/common/crypto/signature.rs index 2f2f6727989..d74cd7d7a37 100644 --- a/runtime/src/common/crypto/signature.rs +++ b/runtime/src/common/crypto/signature.rs @@ -178,6 +178,16 @@ impl Signature { } } +/// Blob signed with one public key. +#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, cbor::Encode, cbor::Decode)] +pub struct Signed { + /// Signed blob. + #[cbor(rename = "untrusted_raw_value")] + pub blob: Vec, + /// Signature over the blob. + pub signature: SignatureBundle, +} + /// Blob signed by multiple public keys. #[derive(Clone, Debug, Default, PartialEq, Eq, Hash, cbor::Encode, cbor::Decode)] pub struct MultiSigned { @@ -197,6 +207,16 @@ pub struct SignatureBundle { pub signature: Signature, } +impl SignatureBundle { + /// Verify returns true iff the signature is valid over the given context + /// and message. + pub fn verify(&self, context: &[u8], message: &[u8]) -> bool { + self.signature + .verify(&self.public_key, context, message) + .is_ok() + } +} + /// A abstract signer. pub trait Signer: Send + Sync { /// Generates a signature over the context and message. diff --git a/runtime/src/config.rs b/runtime/src/config.rs index cdf0382e74e..5854f681dc8 100644 --- a/runtime/src/config.rs +++ b/runtime/src/config.rs @@ -15,6 +15,8 @@ pub struct Config { /// Whether storage state should be persisted between transaction check invocations. The state /// is invalidated on the next round. pub persist_check_tx_state: bool, + /// Whether TEE freshness is verified with freshness proofs. + pub freshness_proofs: bool, } /// Storage-related configuration. diff --git a/runtime/src/consensus/mod.rs b/runtime/src/consensus/mod.rs index 66a11e48992..707d77717dc 100644 --- a/runtime/src/consensus/mod.rs +++ b/runtime/src/consensus/mod.rs @@ -9,6 +9,7 @@ pub mod scheduler; pub mod staking; pub mod state; pub mod tendermint; +pub mod transaction; pub mod verifier; /// The height that represents the most recent block height. diff --git a/runtime/src/consensus/tendermint/merkle.rs b/runtime/src/consensus/tendermint/merkle.rs new file mode 100644 index 00000000000..3c838b6422d --- /dev/null +++ b/runtime/src/consensus/tendermint/merkle.rs @@ -0,0 +1,185 @@ +//! Merkle proofs used in Tendermint networks +//! +//! Rewritten to Rust from: +//! https://github.com/tendermint/tendermint/blob/main/crypto/merkle/proof.go +//! +//! Helper functions copied from: +//! https://github.com/informalsystems/tendermint-rs/blob/main/tendermint/src/merkle.rs + +use std::cmp::Ordering; + +use anyhow::{anyhow, Result}; +use rustc_hex::ToHex; +use sha2::{Digest, Sha256}; + +use tendermint::merkle::{Hash, HASH_SIZE}; + +/// Maximum number of aunts that can be included in a Proof. +/// This corresponds to a tree of size 2^100, which should be sufficient for all conceivable purposes. +/// This maximum helps prevent Denial-of-Service attacks by limiting the size of the proofs. +pub const MAX_AUNTS: usize = 100; + +/// Proof represents a Merkle proof. +/// +/// NOTE: The convention for proofs is to include leaf hashes but to +/// exclude the root hash. +/// This convention is implemented across IAVL range proofs as well. +/// Keep this consistent unless there's a very good reason to change +/// everything. This also affects the generalized proof system as +/// well. +#[derive(Debug, Default, cbor::Encode, cbor::Decode)] +pub struct Proof { + pub total: i64, // Total number of items. + pub index: i64, // Index of item to prove. + pub leaf_hash: Hash, // Hash of item value. + pub aunts: Vec, // Hashes from leaf's sibling to a root's child. +} + +impl Proof { + /// Verify that the Proof proves the root hash. + /// Check index/total manually if needed. + pub fn verify(&self, root_hash: Hash, leaf: Hash) -> Result<()> { + if self.total < 0 { + return Err(anyhow!("proof total must be positive")); + } + if self.index < 0 { + return Err(anyhow!("proof index cannot be negative")); + } + if self.aunts.len() > MAX_AUNTS { + return Err(anyhow!( + "expected no more than {} aunts, got {}", + MAX_AUNTS, + self.aunts.len() + )); + } + + let leaf_hash = leaf_hash(&leaf); + match self.leaf_hash.cmp(&leaf_hash) { + Ordering::Equal => (), + _ => { + return Err(anyhow!( + "invalid leaf hash: wanted {} got {}", + leaf_hash.to_hex::(), + self.leaf_hash.to_hex::(), + )); + } + } + + let computed_hash = self.compute_root_hash().ok_or_else(|| { + anyhow!( + "invalid root hash: wanted {} got None", + root_hash.to_hex::() + ) + })?; + match computed_hash.cmp(&root_hash) { + Ordering::Equal => (), + _ => { + return Err(anyhow!( + "invalid root hash: wanted {} got {}", + root_hash.to_hex::(), + computed_hash.to_hex::(), + )); + } + } + + Ok(()) + } + + /// Compute the root hash given a leaf hash. Does not verify the result. + pub fn compute_root_hash(&self) -> Option { + Self::compute_hash_from_aunts(self.index, self.total, self.leaf_hash, &self.aunts) + } + + /// Use the leaf_hash and inner_hashes to get the root merkle hash. + /// If the length of the inner_hashes slice isn't exactly correct, the result is None. + /// Recursive impl. + fn compute_hash_from_aunts( + index: i64, + total: i64, + leaf_hash: Hash, + inner_hashes: &[Hash], + ) -> Option { + if index >= total || index < 0 || total <= 0 { + return None; + } + match total { + 0 => unreachable!("cannot call compute_hash_from_aunts() with 0 total"), // Handled above. + 1 => { + if !inner_hashes.is_empty() { + return None; + } + Some(leaf_hash) + } + _ => { + if inner_hashes.is_empty() { + return None; + } + let last_idx = inner_hashes.len() - 1; + let last_hash = inner_hashes[last_idx]; + let inner_hashes = &inner_hashes[..last_idx]; + + let num_left = get_split_point(total as usize) as i64; + if index < num_left { + if let Some(left_hash) = + Self::compute_hash_from_aunts(index, num_left, leaf_hash, inner_hashes) + { + return Some(inner_hash(&left_hash, &last_hash)); + } + return None; + } + if let Some(right_hash) = Self::compute_hash_from_aunts( + index - num_left, + total - num_left, + leaf_hash, + inner_hashes, + ) { + return Some(inner_hash(&last_hash, &right_hash)); + } + None + } + } + } +} + +/// returns the largest power of 2 less than length +fn get_split_point(length: usize) -> usize { + match length { + 0 => panic!("tree is empty!"), + 1 => panic!("tree has only one element!"), + 2 => 1, + _ => length.next_power_of_two() / 2, + } +} + +/// tmhash(0x00 || leaf) +fn leaf_hash(bytes: &[u8]) -> Hash { + // make a new array starting with 0 and copy in the bytes + let mut leaf_bytes = Vec::with_capacity(bytes.len() + 1); + leaf_bytes.push(0x00); + leaf_bytes.extend_from_slice(bytes); + + // hash it ! + let digest = Sha256::digest(&leaf_bytes); + + // copy the GenericArray out + let mut hash_bytes = [0u8; HASH_SIZE]; + hash_bytes.copy_from_slice(&digest); + hash_bytes +} + +/// tmhash(0x01 || left || right) +fn inner_hash(left: &[u8], right: &[u8]) -> Hash { + // make a new array starting with 0x1 and copy in the bytes + let mut inner_bytes = Vec::with_capacity(left.len() + right.len() + 1); + inner_bytes.push(0x01); + inner_bytes.extend_from_slice(left); + inner_bytes.extend_from_slice(right); + + // hash it ! + let digest = Sha256::digest(&inner_bytes); + + // copy the GenericArray out + let mut hash_bytes = [0u8; HASH_SIZE]; + hash_bytes.copy_from_slice(&digest); + hash_bytes +} diff --git a/runtime/src/consensus/tendermint/mod.rs b/runtime/src/consensus/tendermint/mod.rs index 9a00c692f5e..8fb9c43a7e4 100644 --- a/runtime/src/consensus/tendermint/mod.rs +++ b/runtime/src/consensus/tendermint/mod.rs @@ -1,5 +1,6 @@ //! Tendermint consensus layer backend. +pub mod merkle; mod store; pub mod verifier; diff --git a/runtime/src/consensus/tendermint/verifier.rs b/runtime/src/consensus/tendermint/verifier.rs index c1ab09b95af..808cfc5ec3b 100644 --- a/runtime/src/consensus/tendermint/verifier.rs +++ b/runtime/src/consensus/tendermint/verifier.rs @@ -10,10 +10,13 @@ use std::{ use anyhow::anyhow; use crossbeam::channel; use io_context::Context; +use rand::{rngs::OsRng, Rng}; use sgx_isa::Keypolicy; +use sha2::{Digest, Sha256}; use slog::{error, info}; use tendermint::{ block::{CommitSig, Height}, + merkle::HASH_SIZE, vote::{SignedVote, ValidatorIndex, Vote}, }; use tendermint_light_client::{ @@ -54,6 +57,7 @@ use crate::{ tendermint::{ decode_light_block, state_root_from_header, LightBlockMeta, TENDERMINT_CONTEXT, }, + transaction::{SignedTransaction, Transaction, SIGNATURE_CONTEXT}, verifier::{self, verify_state_freshness, Error, TrustRoot, TrustedState}, Event, LightBlock, HEIGHT_LATEST, }, @@ -62,7 +66,7 @@ use crate::{ types::{Body, EventKind, HostFetchConsensusEventsRequest, HostFetchConsensusEventsResponse}, }; -use super::{encode_light_block, store::LruStore}; +use super::{encode_light_block, merkle::Proof, store::LruStore}; /// Maximum number of times to retry initialization. const MAX_INITIALIZATION_RETRIES: usize = 3; @@ -75,6 +79,11 @@ const TRUSTED_STATE_STORAGE_KEY_PREFIX: &str = "tendermint.verifier.trusted_stat const TRUSTED_STATE_CONTEXT: &[u8] = b"oasis-core/verifier: trusted state"; /// Trusted state save interval (in consensus blocks). const TRUSTED_STATE_SAVE_INTERVAL: u64 = 128; +/// Size of nonce for prove freshness request. +const NONCE_SIZE: usize = 32; + +/// Nonce for prove freshness request. +type Nonce = [u8; NONCE_SIZE]; /// A verifier which performs no verification. pub struct NopVerifier { @@ -357,7 +366,8 @@ impl Verifier { Ok(untrusted_block) } - fn verify_freshness( + /// Verify state freshness using RAK and nonces. + fn verify_freshness_with_rak( &self, state: &ConsensusState, node_id: &Option, @@ -368,7 +378,106 @@ impl Verifier { return Ok(None); }; - verify_state_freshness(state, &self.trust_root, rak, &self.runtime_version, node_id) + verify_state_freshness( + state, + rak, + &self.trust_root.runtime_id, + &self.runtime_version, + node_id, + ) + } + + /// Verify state freshness using prove freshness transaction. + /// + /// Verification is done in three steps. In the first one, the verifier selects a unique nonce + /// and sends it to the host. The second step is done by the host, who prepares, signs and + /// submits a prove freshness transaction using the received nonce. Once transaction is included + /// in a block, the host replies with block's height, transaction details and a Merkle proof + /// that the transaction was included in the block. In the final step, the verifier verifies + /// the proof and accepts state as fresh iff verification succeeds. + fn verify_freshness_with_proof(&self, instance: &mut Instance) -> Result<(), Error> { + info!( + self.logger, + "Verifying state freshness using prove freshness transaction" + ); + + // Generate a random nonce for prove freshness transaction. + let mut rng = OsRng {}; + let mut nonce = [0u8; NONCE_SIZE]; + rng.fill(&mut nonce); + + // Ask host for freshness proof. + let io = Io::new(&self.protocol); + let (signed_tx, height, merkle_proof) = + io.fetch_freshness_proof(&nonce).map_err(|err| { + Error::FreshnessVerificationFailed(anyhow!( + "failed to fetch freshness proof: {}", + err + )) + })?; + + // Peek into the transaction to verify the nonce and the signature. No need to verify + // the name of the method though. + let tx: Transaction = cbor::from_slice(signed_tx.blob.as_slice()).map_err(|err| { + Error::FreshnessVerificationFailed(anyhow!( + "failed to decode prove freshness transaction: {}", + err + )) + })?; + let tx_nonce: Nonce = cbor::from_value(tx.body).map_err(|err| { + Error::FreshnessVerificationFailed(anyhow!("failed to decode nonce: {}", err)) + })?; + match nonce.cmp(&tx_nonce) { + std::cmp::Ordering::Equal => (), + _ => return Err(Error::FreshnessVerificationFailed(anyhow!("invalid nonce"))), + } + + let chain_context = self.protocol.get_host_info().consensus_chain_context; + let mut context = SIGNATURE_CONTEXT.to_vec(); + context.extend(chain_context.as_bytes()); + if !signed_tx.signature.verify(&context, &signed_tx.blob) { + return Err(Error::FreshnessVerificationFailed(anyhow!( + "failed to verify the signature" + ))); + } + + // Fetch the block in which the transaction was published. + let block = instance + .light_client + .verify_to_target(height.try_into().unwrap(), &mut instance.state) + .map_err(|err| { + Error::FreshnessVerificationFailed(anyhow!("failed to fetch the block: {}", err)) + })?; + + let header = block.signed_header.header; + if header.height.value() != height { + return Err(Error::VerificationFailed(anyhow!("invalid block"))); + } + + // Compute hash of the transaction and verify the proof. + let digest = Sha256::digest(&cbor::to_vec(signed_tx)); + let mut tx_hash = [0u8; HASH_SIZE]; + tx_hash.copy_from_slice(&digest); + + let root_hash = header + .data_hash + .ok_or_else(|| Error::FreshnessVerificationFailed(anyhow!("root hash not found")))?; + let root_hash = match root_hash { + TMHash::Sha256(hash) => hash, + TMHash::None => { + return Err(Error::FreshnessVerificationFailed(anyhow!( + "root hash not found" + ))); + } + }; + + merkle_proof.verify(root_hash, tx_hash).map_err(|err| { + Error::FreshnessVerificationFailed(anyhow!("failed to verify the proof: {}", err)) + })?; + + info!(self.logger, "State freshness successfully verified"); + + Ok(()) } fn verify( @@ -472,7 +581,7 @@ impl Verifier { // Verify our own RAK is published in registry once per epoch. // This ensures consensus state is recent enough. if cache.last_verified_epoch != epoch { - cache.node_id = self.verify_freshness(&state, &cache.node_id)?; + cache.node_id = self.verify_freshness_with_rak(&state, &cache.node_id)?; } // Cache verified runtime header. @@ -847,6 +956,13 @@ impl Verifier { "latest_height" => cache.latest_known_height(), ); + // Verify state freshness with freshness proof. This step is required only for clients + // as executors and key managers verify freshness regularly using node registration + // (RAK with random nonces). + if self.protocol.get_config().freshness_proofs { + self.verify_freshness_with_proof(&mut instance)?; + }; + // Start the command processing loop. loop { let command = self.command_receiver.recv().map_err(|_| Error::Internal)?; @@ -1199,6 +1315,33 @@ impl Io { Ok(height) } + + fn fetch_freshness_proof( + &self, + nonce: &Nonce, + ) -> Result<(SignedTransaction, u64, Proof), IoError> { + let result = self + .protocol + .call_host( + Context::background(), + Body::HostProveFreshnessRequest { + blob: nonce.to_vec(), + }, + ) + .map_err(|err| IoError::rpc(RpcError::server(err.to_string())))?; + + // Extract proof from response. + let (signed_tx, proof) = match result { + Body::HostProveFreshnessResponse { signed_tx, proof } => (signed_tx, proof), + _ => return Err(IoError::rpc(RpcError::server("bad response".to_string()))), + }; + + // Decode raw proof as a Tendermint Merkle proof of inclusion. + let merkle_proof = cbor::from_slice(&proof.raw_proof) + .map_err(|err| IoError::rpc(RpcError::server(err.to_string())))?; + + Ok((signed_tx, proof.height, merkle_proof)) + } } impl components::io::Io for Io { diff --git a/runtime/src/consensus/transaction.rs b/runtime/src/consensus/transaction.rs new file mode 100644 index 00000000000..24105f2ffea --- /dev/null +++ b/runtime/src/consensus/transaction.rs @@ -0,0 +1,46 @@ +use crate::common::{crypto::signature::Signed, quantity::Quantity}; + +pub const SIGNATURE_CONTEXT: &[u8] = b"oasis-core/consensus: tx for chain "; + +/// Unsigned consensus transaction. +#[derive(Debug, cbor::Encode, cbor::Decode)] +#[cbor(no_default)] +pub struct Transaction { + /// Nonce to prevent replay. + pub nonce: u64, + /// Optional fee that the sender commits to pay to execute this transaction. + pub fee: Option, + + /// Method that should be called. + pub method: MethodName, + /// Method call body. + pub body: cbor::Value, +} + +/// Signed consensus transaction. +pub type SignedTransaction = Signed; + +/// Consensus transaction fee the sender wishes to pay for operations which +/// require a fee to be paid to validators. +#[derive(Debug, Default, cbor::Encode, cbor::Decode)] +pub struct Fee { + /// Fee amount to be paid. + pub amount: Quantity, + /// Maximum gas that a transaction can use. + pub gas: Gas, +} + +/// Consensus gas representation. +pub type Gas = u64; + +/// Method name. +pub type MethodName = String; + +/// Proof of transaction inclusion in a block. +#[derive(Debug, Default, cbor::Encode, cbor::Decode)] +pub struct Proof { + /// Block height at which the transaction was published. + pub height: u64, + /// Actual raw proof. + pub raw_proof: Vec, +} diff --git a/runtime/src/consensus/verifier.rs b/runtime/src/consensus/verifier.rs index 1d4c8b8cf43..1c83750a12f 100644 --- a/runtime/src/consensus/verifier.rs +++ b/runtime/src/consensus/verifier.rs @@ -31,6 +31,9 @@ pub enum Error { #[error("consensus chain context transition failed: {0}")] ChainContextTransitionFailed(#[source] anyhow::Error), + #[error("freshness verification: {0}")] + FreshnessVerificationFailed(#[source] anyhow::Error), + #[error("internal consensus verifier error")] Internal, } @@ -42,7 +45,8 @@ impl Error { Error::VerificationFailed(_) => 2, Error::TrustedStateLoadingFailed => 3, Error::ChainContextTransitionFailed(_) => 4, - Error::Internal => 5, + Error::FreshnessVerificationFailed(_) => 5, + Error::Internal => 6, } } } @@ -200,8 +204,8 @@ pub struct TrustedState { /// passed in order to optimize discovery for subsequent runs. pub fn verify_state_freshness( state: &ConsensusState, - trust_root: &TrustRoot, rak: &RAK, + runtime_id: &Namespace, version: &Version, node_id: &Option, ) -> Result, Error> { @@ -224,7 +228,7 @@ pub fn verify_state_freshness( node_id, )) })?; - if !node.has_tee(rak, &trust_root.runtime_id, version) { + if !node.has_tee(rak, runtime_id, version) { return Err(Error::VerificationFailed(anyhow!( "own RAK not found in registry state" ))); @@ -242,7 +246,7 @@ pub fn verify_state_freshness( })?; let mut found_node: Option = None; for node in nodes { - if node.has_tee(rak, &trust_root.runtime_id, version) { + if node.has_tee(rak, runtime_id, version) { found_node = Some(node.id); break; } diff --git a/runtime/src/types.rs b/runtime/src/types.rs index 9bc34a75ca7..eb8388d0b83 100644 --- a/runtime/src/types.rs +++ b/runtime/src/types.rs @@ -17,6 +17,7 @@ use crate::{ self, beacon::EpochTime, roothash::{self, Block, ComputeResultsHeader, Header}, + transaction::{Proof, SignedTransaction}, LightBlock, }, enclave_rpc, @@ -245,6 +246,13 @@ pub enum Body { HostFetchGenesisHeightResponse { height: u64, }, + HostProveFreshnessRequest { + blob: Vec, + }, + HostProveFreshnessResponse { + signed_tx: SignedTransaction, + proof: Proof, + }, } impl Default for Body { diff --git a/tests/runtimes/simple-keyvalue/src/main.rs b/tests/runtimes/simple-keyvalue/src/main.rs index 0f53d36fb11..f240d992eaf 100644 --- a/tests/runtimes/simple-keyvalue/src/main.rs +++ b/tests/runtimes/simple-keyvalue/src/main.rs @@ -419,6 +419,7 @@ pub fn main_with_version(version: Version) { initial_batch_size: MAX_BATCH_SIZE.try_into().unwrap(), }), }), + freshness_proofs: true, ..Default::default() }, );