Skip to content

Commit

Permalink
Merge pull request #16 from EspressoSystems/hotshot-client
Browse files Browse the repository at this point in the history
Implement HotShot commitment reader
  • Loading branch information
nomaxg authored Nov 30, 2023
2 parents 41d2386 + 24c7dc9 commit 5954ec3
Show file tree
Hide file tree
Showing 15 changed files with 227 additions and 35 deletions.
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -157,17 +157,17 @@ jobs:
if: matrix.test-mode == 'defaults'
run: |
packages=`go list ./...`
gotestsum --format short-verbose --packages="$packages" --rerun-fails=1 -- -coverprofile=coverage.txt -covermode=atomic -coverpkg=./...,./go-ethereum/...
gotestsum --format short-verbose --packages="$packages" --rerun-fails=1 -- -timeout 20m -coverprofile=coverage.txt -covermode=atomic -coverpkg=./...,./go-ethereum/...
- name: run tests with race detection
if: matrix.test-mode == 'race'
run: |
packages=`go list ./...`
gotestsum --format short-verbose --packages="$packages" --rerun-fails=1 -- -race
gotestsum --format short-verbose --packages="$packages" --rerun-fails=1 -- --timeout 20m -race
- name: run redis tests
if: matrix.test-mode == 'defaults'
run: TEST_REDIS=redis://redis:6379/0 gotestsum --format short-verbose -- -p 1 -run TestRedis ./arbnode/... ./system_tests/... -coverprofile=coverage-redis.txt -covermode=atomic -coverpkg=./...
run: TEST_REDIS=redis://redis:6379/0 gotestsum --format short-verbose -- -timeout 20m -p 1 -run TestRedis ./arbnode/... ./system_tests/... -coverprofile=coverage-redis.txt -covermode=atomic -coverpkg=./...

- name: run challenge tests
if: matrix.test-mode == 'challenge'
Expand Down
14 changes: 6 additions & 8 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -332,14 +332,6 @@ contracts/test/prover/proofs/%.json: $(arbitrator_cases)/%.wasm $(arbitrator_pro
go run solgen/gen.go
@touch $@

.make/generate-hotshot-binding:
forge build --root espresso-sequencer --out ../out --extra-output-files abi
mkdir -p solgen/go/espressogen/hotshot
mv ./out/HotShot.sol/HotShot.abi.json ./solgen/go/espressogen/hotshot
rm -rf out
abigen --abi ./solgen/go/espressogen/hotshot/HotShot.abi.json --pkg hotshot --out ./solgen/go/espressogen/hotshot/hotshot.go
rm ./solgen/go/espressogen/hotshot/HotShot.abi.json

.make/solidity: $(DEP_PREDICATE) contracts/src/*/*.sol .make/yarndeps $(ORDER_ONLY_PREDICATE) .make
yarn --cwd contracts build
@touch $@
Expand Down Expand Up @@ -370,6 +362,12 @@ contracts/test/prover/proofs/%.json: $(arbitrator_cases)/%.wasm $(arbitrator_pro
.make:
mkdir .make

generate-hotshot-binding:
forge build --root espresso-sequencer --out ../out --extra-output-files abi
mv ./out/HotShot.sol/HotShot.abi.json ./arbos/espresso/hotshot
rm -rf out
abigen --abi ./arbos/espresso/hotshot/HotShot.abi.json --pkg hotshot --out ./arbos/espresso/hotshot/hotshot.go
rm ./arbos/espresso/hotshot/HotShot.abi.json

# Makefile settings

Expand Down
64 changes: 64 additions & 0 deletions arbnode/hotshot_reader.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package arbnode

import (
"fmt"
"math/big"

"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/common"
"github.com/offchainlabs/nitro/arbos/espresso"
"github.com/offchainlabs/nitro/arbos/espresso/hotshot"
)

type HotShotReader struct {
HotShot hotshot.Hotshot
}

func NewHotShotReader(hotShotAddr common.Address, l1client bind.ContractBackend) (*HotShotReader, error) {
hotshot, err := hotshot.NewHotshot(hotShotAddr, l1client)
if err != nil {
return nil, err
}

return &HotShotReader{
HotShot: *hotshot,
}, nil
}

// L1HotShotCommitmentFromHeight returns a HotShot commitments to a sequencer block
// This is used in the derivation pipeline to validate sequencer batches in Espresso mode
func (h *HotShotReader) L1HotShotCommitmentFromHeight(blockHeight uint64) (*espresso.Commitment, error) {
var comm espresso.Commitment
// Check if the requested commitments are even available yet on L1.
contractBlockHeight, err := h.HotShot.HotshotCaller.BlockHeight(nil)
if err != nil {
return nil, err
}
if contractBlockHeight.Cmp(big.NewInt(int64(blockHeight))) < 0 {
return nil, fmt.Errorf("commitment at block height %d is unavailable (current contract block height %d)", blockHeight, contractBlockHeight)
}

commAsInt, err := h.HotShot.HotshotCaller.Commitments(nil, big.NewInt(int64(blockHeight)))
if err != nil {
return nil, err
}
if commAsInt.Cmp(big.NewInt(0)) == 0 {
// A commitment of 0 indicates that this commitment hasn't been set yet in the contract.
// Since we checked the contract block height above, this can only happen if there was
// a reorg on L1 just now. In this case, return an error rather than reporting
// definitive commitments. The caller will retry and we will succeed eventually when we
// manage to get a consistent snapshot of the L1.
//
// Note that in all other reorg cases, where the L1 reorgs but we read a nonzero
// commitment, we are fine, since the HotShot contract will only ever record a single
// ledger, consistent across all L1 forks, determined by HotShot consensus. The only
// question is whether the recorded ledger extends far enough for the commitments we're
// trying to read on the current fork of L1.
return nil, fmt.Errorf("read 0 for commitment %d at block height %d, this indicates an L1 reorg", blockHeight, contractBlockHeight)
}
comm, err = espresso.CommitmentFromUint256(espresso.NewU256().SetBigInt(commAsInt))
if err != nil {
return nil, err
}
return &comm, nil
}
9 changes: 9 additions & 0 deletions arbnode/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -734,9 +734,18 @@ func createNodeImpl(

var statelessBlockValidator *staker.StatelessBlockValidator
if config.BlockValidator.ValidationServer.URL != "" {
var hotShotReader *HotShotReader
if config.BlockValidator.Espresso {
addr := common.HexToAddress(config.BlockValidator.HotShotAddress)
hotShotReader, err = NewHotShotReader(addr, l1client)
if err != nil {
return nil, err
}
}
statelessBlockValidator, err = staker.NewStatelessBlockValidator(
inboxReader,
inboxTracker,
hotShotReader,
txStreamer,
exec,
rawdb.NewTable(arbDb, storage.BlockValidatorPrefix),
Expand Down
5 changes: 3 additions & 2 deletions arbos/espresso/hotshot/hotshot.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 19 additions & 0 deletions config/validation_node_config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"persistent": {
"chain": "local"
},
"ws": {
"addr": ""
},
"http": {
"addr": ""
},
"validation": {
"api-auth": true,
"api-public": false
},
"auth": {
"jwtsecret": "./config/val_jwt.hex",
"addr": "0.0.0.0"
}
}
80 changes: 80 additions & 0 deletions config/validator_config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
{
"parent-chain": {
"connection": {
"url": "ws://localhost:8546"
},
"wallet": {
"account": "0xE46fC174A70D204A737689996F6dA33Ed14e1457",
"password": "passphrase",
"pathname": "/Users/ngolub/.arbitrum/local/keystore"
}
},
"chain": {
"id": 412346,
"info-files": [
"./config/l2_chain_info.json"
]
},
"execution": {
"sequencer": {
"enable": false
}
},
"node": {
"staker": {
"dangerous": {
"without-block-validator": false
},
"disable-challenge": false,
"enable": true,
"staker-interval": "10s",
"make-assertion-interval": "10s",
"strategy": "MakeNodes",
"use-smart-contract-wallet": true
},
"sequencer": false,
"delayed-sequencer": {
"enable": false
},
"seq-coordinator": {
"enable": false,
"redis-url": "redis://localhost:6379",
"lockout-duration": "30s",
"lockout-spare": "1s",
"my-url": "",
"retry-interval": "0.5s",
"seq-num-duration": "24h0m0s",
"update-interval": "3s"
},
"batch-poster": {
"enable": false,
"redis-url": "redis://localhost:6379",
"max-delay": "30s",
"data-poster": {
"redis-signer": {
"signing-key": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
},
"wait-for-l1-finality": false
}
},
"block-validator": {
"validation-server": {
"url": "ws://localhost:8949",
"jwtsecret": "./config/val_jwt.hex"
},
"hotshot-address": "0x217788c286797d56cd59af5e493f3699c39cbbe8",
"espresso": true
}
},
"persistent": {
"chain": "local"
},
"ws": {
"addr": "0.0.0.0"
},
"http": {
"addr": "0.0.0.0",
"vhosts": "*",
"corsdomain": "*"
}
}
2 changes: 1 addition & 1 deletion nitro-testnode
4 changes: 3 additions & 1 deletion staker/block_validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,8 @@ type BlockValidatorConfig struct {
FailureIsFatal bool `koanf:"failure-is-fatal" reload:"hot"`
Dangerous BlockValidatorDangerousConfig `koanf:"dangerous"`
// Espresso specific flags
Espresso bool `koanf:"espresso"`
Espresso bool `koanf:"espresso"`
HotShotAddress string `koanf:"hotshot-address"` //nolint
}

func (c *BlockValidatorConfig) Validate() error {
Expand All @@ -108,6 +109,7 @@ func BlockValidatorConfigAddOptions(prefix string, f *flag.FlagSet) {
f.Uint64(prefix+".prerecorded-blocks", DefaultBlockValidatorConfig.PrerecordedBlocks, "record that many blocks ahead of validation (larger footprint)")
f.String(prefix+".current-module-root", DefaultBlockValidatorConfig.CurrentModuleRoot, "current wasm module root ('current' read from chain, 'latest' from machines/latest dir, or provide hash)")
f.String(prefix+".pending-upgrade-module-root", DefaultBlockValidatorConfig.PendingUpgradeModuleRoot, "pending upgrade wasm module root to additionally validate (hash, 'latest' or empty)")
f.String(prefix+".hotshot-address", DefaultBlockValidatorConfig.HotShotAddress, "hotshot contract address that stores the commitments that must be validated against espresso sequencer batches")
f.Bool(prefix+".failure-is-fatal", DefaultBlockValidatorConfig.FailureIsFatal, "failing a validation is treated as a fatal error")
f.Bool(prefix+".espresso", DefaultBlockValidatorConfig.Espresso, "if true, hotshot header preimages will be added to validation entries to verify that transactions have been sequenced by espresso")
BlockValidatorDangerousConfigAddOptions(prefix+".dangerous", f)
Expand Down
31 changes: 24 additions & 7 deletions staker/stateless_block_validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ type StatelessBlockValidator struct {
db ethdb.Database
daService arbstate.DataAvailabilityReader

hotShotReader HotShotReaderInterface

moduleMutex sync.Mutex
currentWasmModuleRoot common.Hash
pendingWasmModuleRoot common.Hash
Expand Down Expand Up @@ -72,6 +74,10 @@ type InboxReaderInterface interface {
GetSequencerMessageBytes(ctx context.Context, seqNum uint64) ([]byte, error)
}

type HotShotReaderInterface interface {
L1HotShotCommitmentFromHeight(blockHeight uint64) (*espresso.Commitment, error)
}

type L1ReaderInterface interface {
Client() arbutil.L1Interface
Subscribe(bool) (<-chan *types.Header, func())
Expand Down Expand Up @@ -224,13 +230,18 @@ func newValidationEntry(
func NewStatelessBlockValidator(
inboxReader InboxReaderInterface,
inbox InboxTrackerInterface,
hotShotReader HotShotReaderInterface,
streamer TransactionStreamerInterface,
recorder execution.ExecutionRecorder,
arbdb ethdb.Database,
das arbstate.DataAvailabilityReader,
config func() *BlockValidatorConfig,
stack *node.Node,
) (*StatelessBlockValidator, error) {
// Sanity check, also used to surpress the unused koanf field lint error
if config().Espresso && config().HotShotAddress == "" {
return nil, errors.New("cannot create a new stateless block validator in espresso mode without a hotshot reader")
}
valConfFetcher := func() *rpcclient.ClientConfig { return &config().ValidationServer }
valClient := server_api.NewValidationClient(valConfFetcher, stack)
execClient := server_api.NewExecutionClient(valConfFetcher, stack)
Expand All @@ -239,6 +250,7 @@ func NewStatelessBlockValidator(
execSpawner: execClient,
recorder: recorder,
validationSpawners: []validator.ValidationSpawner{valClient},
hotShotReader: hotShotReader,
inboxReader: inboxReader,
inboxTracker: inbox,
streamer: streamer,
Expand Down Expand Up @@ -291,6 +303,18 @@ func (v *StatelessBlockValidator) ValidationEntryRecord(ctx context.Context, e *
e.DelayedMsg = delayedMsg
}
for i, batch := range e.BatchInfo {
if usingEspresso {
// TODO: Use the inbox tracker to fetch the correct HotShot index
// https://github.com/EspressoSystems/espresso-sequencer/issues/782
batchNum := batch.Number
hotShotCommitment, err := v.hotShotReader.L1HotShotCommitmentFromHeight(batchNum)
if err != nil {
return fmt.Errorf("error attempting to fetch HotShot commitment for block %d: %w", batchNum, err)

}
log.Info("fetched HotShot commitment", "batchNum", batchNum, "commitment", hotShotCommitment)
e.BatchInfo[i].HotShotCommitment = *hotShotCommitment
}
if len(batch.Data) <= 40 {
continue
}
Expand All @@ -307,13 +331,6 @@ func (v *StatelessBlockValidator) ValidationEntryRecord(ctx context.Context, e *
return err
}
}
if usingEspresso {
// TODO: implement client method to fetch real headers
// https://github.com/EspressoSystems/espresso-sequencer/issues/771
hotShotHeader := espresso.Header{}
hotShotCommitment := hotShotHeader.Commit()
e.BatchInfo[i].HotShotCommitment = &hotShotCommitment
}
}

e.msg = nil // no longer needed
Expand Down
4 changes: 2 additions & 2 deletions system_tests/full_challenge_impl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,7 @@ func RunChallengeTest(t *testing.T, asserterIsCorrect bool, useStubs bool, chall

confirmLatestBlock(ctx, t, l1Info, l1Backend)

asserterValidator, err := staker.NewStatelessBlockValidator(asserterL2.InboxReader, asserterL2.InboxTracker, asserterL2.TxStreamer, asserterExec.Recorder, asserterL2ArbDb, nil, StaticFetcherFrom(t, &conf.BlockValidator), valStack)
asserterValidator, err := staker.NewStatelessBlockValidator(asserterL2.InboxReader, asserterL2.InboxTracker, nil, asserterL2.TxStreamer, asserterExec.Recorder, asserterL2ArbDb, nil, StaticFetcherFrom(t, &conf.BlockValidator), valStack)
if err != nil {
Fatal(t, err)
}
Expand All @@ -390,7 +390,7 @@ func RunChallengeTest(t *testing.T, asserterIsCorrect bool, useStubs bool, chall
if err != nil {
Fatal(t, err)
}
challengerValidator, err := staker.NewStatelessBlockValidator(challengerL2.InboxReader, challengerL2.InboxTracker, challengerL2.TxStreamer, challengerExec.Recorder, challengerL2ArbDb, nil, StaticFetcherFrom(t, &conf.BlockValidator), valStack)
challengerValidator, err := staker.NewStatelessBlockValidator(challengerL2.InboxReader, challengerL2.InboxTracker, nil, challengerL2.TxStreamer, challengerExec.Recorder, challengerL2ArbDb, nil, StaticFetcherFrom(t, &conf.BlockValidator), valStack)
if err != nil {
Fatal(t, err)
}
Expand Down
2 changes: 2 additions & 0 deletions system_tests/staker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ func stakerTestImpl(t *testing.T, faultyStaker bool, honestStakerInactive bool)
statelessA, err := staker.NewStatelessBlockValidator(
l2nodeA.InboxReader,
l2nodeA.InboxTracker,
nil,
l2nodeA.TxStreamer,
execNodeA,
l2nodeA.ArbDB,
Expand Down Expand Up @@ -194,6 +195,7 @@ func stakerTestImpl(t *testing.T, faultyStaker bool, honestStakerInactive bool)
statelessB, err := staker.NewStatelessBlockValidator(
l2nodeB.InboxReader,
l2nodeB.InboxTracker,
nil,
l2nodeB.TxStreamer,
execNodeB,
l2nodeB.ArbDB,
Expand Down
Loading

0 comments on commit 5954ec3

Please sign in to comment.