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 19, 2024
1 parent 0626f3b commit 95d2dc4
Show file tree
Hide file tree
Showing 3 changed files with 270 additions and 0 deletions.
104 changes: 104 additions & 0 deletions cmd/cartesi-rollups-node/machinehash.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// (c) Cartesi and individual authors (see AUTHORS)
// SPDX-License-Identifier: Apache-2.0 (see LICENSE)

package main

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(
"cartesi machine 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 "", ReadHashError{Err: err}
} else if len(hash) != common.HashLength {
return "", MalformedHashError{ReadBytes: 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 "", err
}
cartesiApplication, err := contracts.NewCartesiDAppCaller(
common.HexToAddress(applicationAddress),
client,
)
if err != nil {
return "", err
}
hash, err := cartesiApplication.GetTemplateHash(&bind.CallOpts{Context: ctx})
if err != nil {
return "", err
}
return common.Bytes2Hex(hash[:]), nil
}

// ------------------------------------------------------------------------------------------------
// Custom errors
// ------------------------------------------------------------------------------------------------

type ReadHashError struct {
Err error
}

func (e ReadHashError) Error() string {
return "failed to read hash file: " + e.Err.Error()
}

type MalformedHashError struct {
ReadBytes int
}

func (e MalformedHashError) Error() string {
return fmt.Sprintf(
"malformed hash: expected %v bytes, read %v",
common.HashLength,
e.ReadBytes,
)
}
156 changes: 156 additions & 0 deletions cmd/cartesi-rollups-node/machinehash_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
// (c) Cartesi and individual authors (see AUTHORS)
// SPDX-License-Identifier: Apache-2.0 (see LICENSE)

package main

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"
)

type ValidateMachineHashSuite struct {
suite.Suite
}

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

func (s *ValidateMachineHashSuite) TestItFailsWhenSnapshotHasNoHash() {
machineDir, err := os.MkdirTemp("", "")
if err != nil {
s.FailNow(err.Error())
}

err = validateMachineHash(context.Background(), machineDir, "", "")
s.NotNil(err)
s.IsType(ReadHashError{}, err)

os.RemoveAll(machineDir)
}

func (s *ValidateMachineHashSuite) TestItFailsWhenHashHasWrongSize() {
machineDir, err := mockMachineDir("deadbeef")
if err != nil {
s.FailNow(err.Error())
}

err = validateMachineHash(context.Background(), machineDir, "", "")
s.NotNil(err)
s.IsType(MalformedHashError{}, err)

os.RemoveAll(machineDir)
}

func (s *ValidateMachineHashSuite) TestItFailsWhenContextIsCanceled() {
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
machineDir, err := createMachineSnapshot()
if err != nil {
s.FailNow(err.Error())
}
devnet, err := startDevnet()
if err != nil {
s.FailNow(err.Error())
}

err = validateMachineHash(
ctx,
machineDir,
addresses.GetTestBook().CartesiDApp.String(),
"http://0.0.0.0:"+deps.DefaultDevnetPort,
)
s.NotNil(err)
s.ErrorIs(err, context.DeadlineExceeded)

os.RemoveAll(machineDir)
err = deps.Terminate(context.Background(), devnet)
if err != nil {
s.FailNow(err.Error())
}
}

func (s *ValidateMachineHashSuite) TestItSucceedsWhenHashesAreEqual() {
ctx := context.Background()
machineDir, err := createMachineSnapshot()
if err != nil {
s.FailNow(err.Error())
}
devnet, err := startDevnet()
if err != nil {
s.FailNow(err.Error())
}

if err := validateMachineHash(
ctx,
machineDir,
addresses.GetTestBook().CartesiDApp.String(),
"http://0.0.0.0:"+deps.DefaultDevnetPort,
); err != nil {
s.FailNow(err.Error())
}

os.RemoveAll(machineDir)
err = deps.Terminate(ctx, devnet)
if err != nil {
s.FailNow(err.Error())
}
}

// ------------------------------------------------------------------------------------------------
// 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 return
// 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
}
10 changes: 10 additions & 0 deletions cmd/cartesi-rollups-node/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,16 @@ func main() {
); err != nil {
config.ErrorLogger.Fatal(err)
}
if !config.GetCartesiFeatureDisableMachineHashCheck() {
if err := validateMachineHash(
ctx,
config.GetCartesiSnapshotDir(),
config.GetCartesiContractsApplicationAddress(),
config.GetCartesiBlockchainHttpEndpoint(),
); err != nil {
config.ErrorLogger.Fatal(err)
}
}

sunodoValidatorEnabled := config.GetCartesiExperimentalSunodoValidatorEnabled()
if !sunodoValidatorEnabled {
Expand Down

0 comments on commit 95d2dc4

Please sign in to comment.