Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

runtime/consensus/tendermint/verifier: Support same-block validation #5300

Merged
merged 4 commits into from
Jul 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changelog/5300.feature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
runtime/consensus/tendermint/verifier: Support same-block validation

The post-execution state of the latest consensus block is now verified
using the block metadata transaction, effectively eliminating any block
delay for state verification.
12 changes: 12 additions & 0 deletions go/consensus/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,10 @@ type ClientBackend interface {
// height.
GetTransactionsWithResults(ctx context.Context, height int64) (*TransactionsWithResults, error)

// GetTransactionsWithProofs returns a list of all transactions and their proofs of inclusion
// contained within a consensus block at a specific height.
GetTransactionsWithProofs(ctx context.Context, height int64) (*TransactionsWithProofs, error)

// GetUnconfirmedTransactions returns a list of transactions currently in the local node's
// mempool. These have not yet been included in a block.
GetUnconfirmedTransactions(ctx context.Context) ([][]byte, error)
Expand Down Expand Up @@ -421,3 +425,11 @@ type TransactionsWithResults struct {
Transactions [][]byte `json:"transactions"`
Results []*results.Result `json:"results"`
}

// TransactionsWithProofs is GetTransactionsWithProofs response.
//
// Proofs[i] is a proof of block inclusion for Transactions[i].
type TransactionsWithProofs struct {
Transactions [][]byte `json:"transactions"`
Proofs [][]byte `json:"proofs"`
}
37 changes: 37 additions & 0 deletions go/consensus/api/grpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ var (
methodGetTransactions = serviceName.NewMethod("GetTransactions", int64(0))
// methodGetTransactionsWithResults is the GetTransactionsWithResults method.
methodGetTransactionsWithResults = serviceName.NewMethod("GetTransactionsWithResults", int64(0))
// methodGetTransactionsWithProofs is the GetTransactionsWithProofs method.
methodGetTransactionsWithProofs = serviceName.NewMethod("GetTransactionsWithProofs", int64(0))
// methodGetUnconfirmedTransactions is the GetUnconfirmedTransactions method.
methodGetUnconfirmedTransactions = serviceName.NewMethod("GetUnconfirmedTransactions", nil)
// methodGetGenesisDocument is the GetGenesisDocument method.
Expand Down Expand Up @@ -117,6 +119,10 @@ var (
MethodName: methodGetTransactionsWithResults.ShortName(),
Handler: handlerGetTransactionsWithResults,
},
{
MethodName: methodGetTransactionsWithProofs.ShortName(),
Handler: handlerGetTransactionsWithProofs,
},
{
MethodName: methodGetUnconfirmedTransactions.ShortName(),
Handler: handlerGetUnconfirmedTransactions,
Expand Down Expand Up @@ -421,6 +427,29 @@ func handlerGetTransactionsWithResults(
return interceptor(ctx, height, info, handler)
}

func handlerGetTransactionsWithProofs(
srv interface{},
ctx context.Context,
dec func(interface{}) error,
interceptor grpc.UnaryServerInterceptor,
) (interface{}, error) {
var height int64
if err := dec(&height); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(ClientBackend).GetTransactionsWithProofs(ctx, height)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: methodGetTransactionsWithProofs.FullName(),
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(ClientBackend).GetTransactionsWithProofs(ctx, req.(int64))
}
return interceptor(ctx, height, info, handler)
}

func handlerGetUnconfirmedTransactions(
srv interface{},
ctx context.Context,
Expand Down Expand Up @@ -748,6 +777,14 @@ func (c *consensusClient) GetTransactionsWithResults(ctx context.Context, height
return &rsp, nil
}

func (c *consensusClient) GetTransactionsWithProofs(ctx context.Context, height int64) (*TransactionsWithProofs, error) {
var rsp TransactionsWithProofs
if err := c.conn.Invoke(ctx, methodGetTransactionsWithProofs.FullName(), height, &rsp); err != nil {
return nil, err
}
return &rsp, nil
}

func (c *consensusClient) GetUnconfirmedTransactions(ctx context.Context) ([][]byte, error) {
var rsp [][]byte
if err := c.conn.Invoke(ctx, methodGetUnconfirmedTransactions.FullName(), nil, &rsp); err != nil {
Expand Down
29 changes: 29 additions & 0 deletions go/consensus/cometbft/full/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ package full

import (
"context"
"crypto/sha256"
"fmt"
"sync"
"sync/atomic"

dbm "github.com/cometbft/cometbft-db"
cmtmerkle "github.com/cometbft/cometbft/crypto/merkle"
cmtcore "github.com/cometbft/cometbft/rpc/core"
cmtcoretypes "github.com/cometbft/cometbft/rpc/core/types"
cmtrpctypes "github.com/cometbft/cometbft/rpc/jsonrpc/types"
Expand All @@ -16,6 +18,7 @@ import (
cmttypes "github.com/cometbft/cometbft/types"

beaconAPI "github.com/oasisprotocol/oasis-core/go/beacon/api"
"github.com/oasisprotocol/oasis-core/go/common/cbor"
"github.com/oasisprotocol/oasis-core/go/common/crypto/signature"
"github.com/oasisprotocol/oasis-core/go/common/identity"
"github.com/oasisprotocol/oasis-core/go/common/logging"
Expand Down Expand Up @@ -733,6 +736,32 @@ func (n *commonNode) GetTransactionsWithResults(ctx context.Context, height int6
return &txsWithResults, nil
}

// Implements consensusAPI.Backend.
func (n *commonNode) GetTransactionsWithProofs(ctx context.Context, height int64) (*consensusAPI.TransactionsWithProofs, error) {
txs, err := n.GetTransactions(ctx, height)
if err != nil {
return nil, err
}

// CometBFT 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 := cmtmerkle.ProofsFromByteSlices(hashes)
rawProofs := make([][]byte, 0, len(proofs))
for _, p := range proofs {
rawProofs = append(rawProofs, cbor.Marshal(p))
}

return &consensusAPI.TransactionsWithProofs{
Transactions: txs,
Proofs: rawProofs,
}, nil
}

// Implements consensusAPI.Backend.
func (n *commonNode) State() syncer.ReadSyncer {
return n.mux.State().Storage()
Expand Down
17 changes: 3 additions & 14 deletions go/consensus/cometbft/full/full.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ package full
import (
"bytes"
"context"
"crypto/sha256"
"fmt"
"math/rand"
"path/filepath"
Expand All @@ -16,7 +15,6 @@ import (
dbm "github.com/cometbft/cometbft-db"
cmtabcitypes "github.com/cometbft/cometbft/abci/types"
cmtconfig "github.com/cometbft/cometbft/config"
cmtmerkle "github.com/cometbft/cometbft/crypto/merkle"
cmtpubsub "github.com/cometbft/cometbft/libs/pubsub"
cmtlight "github.com/cometbft/cometbft/light"
cmtmempool "github.com/cometbft/cometbft/mempool"
Expand Down Expand Up @@ -208,27 +206,18 @@ func (t *fullService) SubmitTxWithProof(ctx context.Context, tx *transaction.Sig
return nil, err
}

txs, err := t.GetTransactions(ctx, data.Height)
tps, err := t.GetTransactionsWithProofs(ctx, data.Height)
if err != nil {
return nil, err
}

if data.Index >= uint32(len(txs)) {
if data.Index >= uint32(len(tps.Transactions)) {
return nil, fmt.Errorf("cometbft: invalid transaction index")
}

// CometBFT 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 := cmtmerkle.ProofsFromByteSlices(hashes)

return &transaction.Proof{
Height: data.Height,
RawProof: cbor.Marshal(proofs[data.Index]),
RawProof: tps.Proofs[data.Index],
}, nil
}

Expand Down
14 changes: 14 additions & 0 deletions go/consensus/tests/tester.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ func ConsensusImplementationTests(t *testing.T, backend consensus.ClientBackend)

txs, err := backend.GetTransactions(ctx, status.LatestHeight)
require.NoError(err, "GetTransactions")
require.NotEmpty(txs, "number of transactions should be greater than zero")

txsWithResults, err := backend.GetTransactionsWithResults(ctx, status.LatestHeight)
require.NoError(err, "GetTransactionsWithResults")
Expand All @@ -75,6 +76,19 @@ func ConsensusImplementationTests(t *testing.T, backend consensus.ClientBackend)
"GetTransactionsWithResults.Results length mismatch",
)

txsWithProofs, err := backend.GetTransactionsWithProofs(ctx, status.LatestHeight)
require.NoError(err, "GetTransactionsWithProofs")
require.Len(
txsWithProofs.Transactions,
len(txs),
"GetTransactionsWithProofs.Transactions length mismatch",
)
require.Len(
txsWithProofs.Proofs,
len(txsWithProofs.Transactions),
"GetTransactionsWithProofs.Proofs length mismatch",
)

_, err = backend.GetUnconfirmedTransactions(ctx)
require.NoError(err, "GetUnconfirmedTransactions")

Expand Down
14 changes: 14 additions & 0 deletions go/oasis-test-runner/scenario/e2e/runtime/runtime_client_kv.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,20 @@ func (cli *KVTestClient) workload(ctx context.Context) error {

func (cli *KVTestClient) submit(ctx context.Context, req interface{}, rng rand.Source64) error {
switch req := req.(type) {
case KeyValueQuery:
rsp, err := cli.sc.submitKeyValueRuntimeGetQuery(
ctx,
runtimeID,
req.Key,
req.Round,
)
if err != nil {
return fmt.Errorf("failed to query k/v pair: %w", err)
}
if rsp != req.Response {
return fmt.Errorf("response does not have expected value (got: '%v', expected: '%v')", rsp, req.Response)
}

case InsertKeyValueTx:
rsp, err := cli.sc.submitKeyValueRuntimeInsertTx(
ctx,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,14 @@ func newSimpleKeyValueScenario(repeat bool) TestClientScenario {
}
}

// KeyValueQuery queries the value stored under the given key for the specified round from
// the database, and verifies that the response (current value) contains the expected data.
type KeyValueQuery struct {
Key string
Response string
Round uint64
}

// InsertKeyValueTx inserts a key/value pair to the database, and verifies that the response
// (previous value) contains the expected data.
type InsertKeyValueTx struct {
Expand Down
21 changes: 20 additions & 1 deletion go/oasis-test-runner/scenario/e2e/runtime/trust_root.go
Original file line number Diff line number Diff line change
Expand Up @@ -354,5 +354,24 @@ func (sc *TrustRootImpl) Run(childEnv *env.Env) (err error) {
return err
}

return nil
// Run the test client again to verify that queries work correctly immediately after
// the transactions have been published.
queries := make([]interface{}, 0)
for i := 0; i < 5; i++ {
key := fmt.Sprintf("my_key_%d", i)
value := fmt.Sprintf("my_value_%d", i)

queries = append(queries,
InsertKeyValueTx{key, value, "", false, 0},
KeyValueQuery{key, value, roothash.RoundLatest},
)
}

sc.Logger.Info("starting a second test client to check if queries for the last round work")
sc.Scenario.testClient = NewKVTestClient().WithSeed("seed2").WithScenario(NewTestClientScenario(queries))
if err := sc.startTestClientOnly(ctx, childEnv); err != nil {
return err
}

return sc.waitTestClient()
}
21 changes: 21 additions & 0 deletions go/runtime/host/protocol/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ type Body struct {
HostFetchTxBatchResponse *HostFetchTxBatchResponse `json:",omitempty"`
HostFetchGenesisHeightRequest *HostFetchGenesisHeightRequest `json:",omitempty"`
HostFetchGenesisHeightResponse *HostFetchGenesisHeightResponse `json:",omitempty"`
HostFetchBlockMetadataTxRequest *HostFetchBlockMetadataTxRequest `json:",omitempty"`
HostFetchBlockMetadataTxResponse *HostFetchBlockMetadataTxResponse `json:",omitempty"`
HostProveFreshnessRequest *HostProveFreshnessRequest `json:",omitempty"`
HostProveFreshnessResponse *HostProveFreshnessResponse `json:",omitempty"`
HostIdentityRequest *HostIdentityRequest `json:",omitempty"`
Expand Down Expand Up @@ -185,6 +187,9 @@ type Features struct {
// KeyManagerMasterSecretRotation is a feature specifying that the runtime supports rotating
// key manager's master secret.
KeyManagerMasterSecretRotation bool `json:"key_manager_master_secret_rotation,omitempty"`
// SameBlockConsensusValidation is a feature specifying that the runtime supports same-block
// consensus validation.
SameBlockConsensusValidation bool `json:"same_block_consensus_validation,omitempty"`
}

// HasScheduleControl returns true when the runtime supports the schedule control feature.
Expand Down Expand Up @@ -584,6 +589,22 @@ type HostFetchTxBatchResponse struct {
Batch [][]byte `json:"batch,omitempty"`
}

// HostFetchBlockMetadataTxRequest is a request to the host to fetch the block metadata transaction
// for the specified height, along with a proof of inclusion.
type HostFetchBlockMetadataTxRequest struct {
// Height is the consensus block height.
Height uint64 `json:"height"`
}

// HostFetchBlockMetadataTxResponse is a response from the host fetching the block metadata
// transaction, along with a proof of inclusion.
type HostFetchBlockMetadataTxResponse struct {
// SignedTx is a signed block metadata transaction.
SignedTx *consensusTx.SignedTransaction `json:"signed_tx"`
// Proof of transaction inclusion in a block.
Proof *consensusTx.Proof `json:"proof"`
}

// HostProveFreshnessRequest is a request to host to prove state freshness.
type HostProveFreshnessRequest struct {
Blob [32]byte `json:"blob"`
Expand Down
45 changes: 45 additions & 0 deletions go/runtime/registry/host.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,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"
"github.com/oasisprotocol/oasis-core/go/consensus/api/transaction"
consensusResults "github.com/oasisprotocol/oasis-core/go/consensus/api/transaction/results"
keymanager "github.com/oasisprotocol/oasis-core/go/keymanager/api"
registry "github.com/oasisprotocol/oasis-core/go/registry/api"
Expand Down Expand Up @@ -381,6 +382,47 @@ func (h *runtimeHostHandler) handleHostFetchTxBatch(
return &protocol.HostFetchTxBatchResponse{Batch: raw}, nil
}

func (h *runtimeHostHandler) handleHostFetchBlockMetadataTx(
ctx context.Context,
rq *protocol.HostFetchBlockMetadataTxRequest,
) (*protocol.HostFetchBlockMetadataTxResponse, error) {
tps, err := h.consensus.GetTransactionsWithProofs(ctx, int64(rq.Height))
if err != nil {
return nil, err
}

// The block metadata transaction should be located at the end of the block.
for i := len(tps.Transactions) - 1; i >= 0; i-- {
rawTx := tps.Transactions[i]

var sigTx transaction.SignedTransaction
if err = cbor.Unmarshal(rawTx, &sigTx); err != nil {
continue
}

// Signature already verified by the validators, skipping.

var tx transaction.Transaction
if err = cbor.Unmarshal(sigTx.Blob, &tx); err != nil {
continue
}

if tx.Method != consensus.MethodMeta {
continue
}

return &protocol.HostFetchBlockMetadataTxResponse{
SignedTx: &sigTx,
Proof: &transaction.Proof{
Height: int64(rq.Height),
RawProof: tps.Proofs[i],
},
}, nil
}

return nil, fmt.Errorf("block metadata transaction not found")
}

func (h *runtimeHostHandler) handleHostProveFreshness(
ctx context.Context,
rq *protocol.HostProveFreshnessRequest,
Expand Down Expand Up @@ -447,6 +489,9 @@ func (h *runtimeHostHandler) Handle(ctx context.Context, rq *protocol.Body) (*pr
case rq.HostFetchTxBatchRequest != nil:
// Transaction pool.
rsp.HostFetchTxBatchResponse, err = h.handleHostFetchTxBatch(ctx, rq.HostFetchTxBatchRequest)
case rq.HostFetchBlockMetadataTxRequest != nil:
// Block metadata.
rsp.HostFetchBlockMetadataTxResponse, err = h.handleHostFetchBlockMetadataTx(ctx, rq.HostFetchBlockMetadataTxRequest)
case rq.HostProveFreshnessRequest != nil:
// Prove freshness.
rsp.HostProveFreshnessResponse, err = h.handleHostProveFreshness(ctx, rq.HostProveFreshnessRequest)
Expand Down