Skip to content

Commit

Permalink
feat: validate cartesi machine hash
Browse files Browse the repository at this point in the history
  • Loading branch information
torives committed Mar 22, 2024
1 parent 3a5d76a commit 60c4ea6
Show file tree
Hide file tree
Showing 6 changed files with 241 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- Added verification to ensure `CARTESI_BLOCKCHAIN_ID` matches the id returned from the Ethereum node
- Added verification to ensure the Cartesi Machine snapshot hash matches the template hash from the CartesiDApp contract
- Added support for `CARTESI_AUTH_PRIVATE_KEY` and `CARTESI_AUTH_PRIVATE_KEY_FILE`
- Added `CARTESI_AUTH_KIND` environment variable to select the blockchain authetication method
- Added structured logging with slog. Colored logs can now be enabled with `CARTESI_LOG_PRETTY` environment variable.
Expand Down
1 change: 1 addition & 0 deletions build/compose-devnet.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,6 @@ services:
CARTESI_CONTRACTS_INPUT_BOX_ADDRESS: "0x59b22D57D4f067708AB0c00552767405926dc768"
CARTESI_CONTRACTS_INPUT_BOX_DEPLOYMENT_BLOCK_NUMBER: "20"
CARTESI_EPOCH_DURATION: "120"
CARTESI_FEATURE_DISABLE_MACHINE_HASH_CHECK: "true"
CARTESI_AUTH_KIND: "mnemonic"
CARTESI_AUTH_MNEMONIC: "test test test test test test test test test test test junk"
1 change: 1 addition & 0 deletions build/compose-host.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ services:
- "10007:10007" # Host Runner Rollup API
environment:
CARTESI_FEATURE_HOST_MODE: "true"
CARTESI_FEATURE_DISABLE_MACHINE_HASH_CHECK: "true"
84 changes: 84 additions & 0 deletions internal/node/machinehash.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// (c) Cartesi and individual authors (see AUTHORS)
// SPDX-License-Identifier: Apache-2.0 (see LICENSE)

package node

import (
"context"
"fmt"
"os"
"path"

"github.com/cartesi/rollups-node/pkg/contracts"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/ethclient"
)

// Validates if the hash from the Cartesi Machine at machineDir matches the template hash onchain.
// It returns an error if it doesn't.
func validateMachineHash(
ctx context.Context,
machineDir string,
applicationAddress string,
ethereumNodeAddr string,
) error {
offchainHash, err := readHash(machineDir)
if err != nil {
return err
}
onchainHash, err := getTemplateHash(ctx, applicationAddress, ethereumNodeAddr)
if err != nil {
return err
}
if offchainHash != onchainHash {
return fmt.Errorf(
"validate machine hash: hash mismatch; expected %v but got %v",
onchainHash,
offchainHash,
)
}
return nil
}

// Reads the Cartesi Machine hash from machineDir. Returns it as a hex string or
// an error
func readHash(machineDir string) (string, error) {
path := path.Join(machineDir, "hash")
hash, err := os.ReadFile(path)
if err != nil {
return "", fmt.Errorf("read hash: %w", err)
} else if len(hash) != common.HashLength {
return "", fmt.Errorf(
"read hash: wrong size; expected %v bytes but read %v",
common.HashLength,
len(hash),
)
}
return common.Bytes2Hex(hash), nil
}

// Retrieves the template hash from the application contract. Returns it as a
// hex string or an error
func getTemplateHash(
ctx context.Context,
applicationAddress string,
ethereumNodeAddr string,
) (string, error) {
client, err := ethclient.DialContext(ctx, ethereumNodeAddr)
if err != nil {
return "", fmt.Errorf("get template hash: %w", err)
}
cartesiApplication, err := contracts.NewCartesiDAppCaller(
common.HexToAddress(applicationAddress),
client,
)
if err != nil {
return "", fmt.Errorf("get template hash: %w", err)
}
hash, err := cartesiApplication.GetTemplateHash(&bind.CallOpts{Context: ctx})
if err != nil {
return "", fmt.Errorf("get template hash: %w", err)
}
return common.Bytes2Hex(hash[:]), nil
}
143 changes: 143 additions & 0 deletions internal/node/machinehash_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
// (c) Cartesi and individual authors (see AUTHORS)
// SPDX-License-Identifier: Apache-2.0 (see LICENSE)

package node

import (
"context"
"os"
"testing"
"time"

"github.com/cartesi/rollups-node/internal/deps"
"github.com/cartesi/rollups-node/internal/machine"
"github.com/cartesi/rollups-node/pkg/addresses"
"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/suite"
)

const BlockchainHttpEndpoint = "http://0.0.0.0:" + deps.DefaultDevnetPort

type ValidateMachineHashSuite struct {
suite.Suite
}

func TestValidateMachineHash(t *testing.T) {
suite.Run(t, new(ValidateMachineHashSuite))
}

func (s *ValidateMachineHashSuite) TestItFailsWhenSnapshotHasNoHash() {
machineDir, err := os.MkdirTemp("", "")
s.Require().Nil(err)
defer os.RemoveAll(machineDir)

err = validateMachineHash(context.Background(), machineDir, "", "")
s.ErrorContains(err, "no such file or directory")
}

func (s *ValidateMachineHashSuite) TestItFailsWhenHashHasWrongSize() {
machineDir, err := mockMachineDir("deadbeef")
s.Require().Nil(err)
defer os.RemoveAll(machineDir)

err = validateMachineHash(context.Background(), machineDir, "", "")
s.ErrorContains(err, "wrong size")
}

func (s *ValidateMachineHashSuite) TestItFailsWhenContextIsCanceled() {
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()

machineDir, err := createMachineSnapshot()
s.Require().Nil(err)
defer os.RemoveAll(machineDir)

devnet, err := startDevnet()
s.Require().Nil(err)
defer func() {
err = deps.Terminate(context.Background(), devnet)
s.Nil(err)
}()

err = validateMachineHash(
ctx,
machineDir,
addresses.GetTestBook().CartesiDApp.String(),
BlockchainHttpEndpoint,
)
s.NotNil(err)
s.ErrorIs(err, context.DeadlineExceeded)
}

func (s *ValidateMachineHashSuite) TestItSucceedsWhenHashesAreEqual() {
ctx := context.Background()

machineDir, err := createMachineSnapshot()
s.Require().Nil(err)
defer os.RemoveAll(machineDir)

devnet, err := startDevnet()
s.Require().Nil(err)
defer func() {
err = deps.Terminate(context.Background(), devnet)
s.Nil(err)
}()

err = validateMachineHash(
ctx,
machineDir,
addresses.GetTestBook().CartesiDApp.String(),
BlockchainHttpEndpoint,
)
s.Nil(err)
}

// ------------------------------------------------------------------------------------------------
// Auxiliary functions
// ------------------------------------------------------------------------------------------------

// Mocks the Cartesi Machine directory by creating a temporary directory with
// a single file named "hash" with the contents of `hash`, a hexadecimal string
func mockMachineDir(hash string) (string, error) {
temp, err := os.MkdirTemp("", "")
if err != nil {
return "", err
}
hashFile := temp + "/hash"
err = os.WriteFile(hashFile, common.FromHex(hash), os.ModePerm)
if err != nil {
return "", err
}
return temp, nil
}

// Generates a new Cartesi Machine snapshot in a temporary directory and returns
// its path
func createMachineSnapshot() (string, error) {
tmpDir, err := os.MkdirTemp("", "")
if err != nil {
return "", err
}
if err = machine.Save(
"cartesi/rollups-node-snapshot:devel",
tmpDir,
"snapshotTemp",
); err != nil {
return "", err
}
return tmpDir, nil
}

// Starts a devnet in a Docker container with the default parameters
func startDevnet() (*deps.DepsContainers, error) {
container, err := deps.Run(context.Background(), deps.DepsConfig{
Devnet: &deps.DevnetConfig{
DockerImage: deps.DefaultDevnetDockerImage,
Port: deps.DefaultDevnetPort,
},
})
if err != nil {
return nil, err
}
return container, nil
}
11 changes: 11 additions & 0 deletions internal/node/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,17 @@ func Setup(ctx context.Context, c config.NodeConfig) (services.Service, error) {
return nil, err
}

if !c.FeatureDisableMachineHashCheck {
if err := validateMachineHash(
ctx,
c.SnapshotDir,
c.ContractsApplicationAddress,
c.BlockchainHttpEndpoint.Value,
); err != nil {
return nil, err
}
}

// create service
return newSupervisorService(c), nil
}

0 comments on commit 60c4ea6

Please sign in to comment.