Skip to content

Commit

Permalink
hVM Phase 0:
Browse files Browse the repository at this point in the history
op-geth operates two TBC instances, a lightweight (header-only) instance which it uses to track Bitcoin consensus as known by the Hemi protocol based on L2 blocks, and a full instance which synchronizes with the P2P network and is used to index the Bitcoin chain.

Hemi creates "Bitcoin Attributes Deposited" transactions which communicate new Bitcoin headers to be "known" by the Hemi protocol. The protocol uses these headers to maintain a complete, lightweight view of Bitcoin consensus that is synchronized across all Hemi nodes regardless of their view of Bitcoin's P2P network.

When Hemi's lightweight view is advanced by new information as part of the Bitcoin derivation process, Hemi's embedded full node proceeds to index along the canonical tip of the lightweight view, always staying 2 blocks behind the known tip to prevent a data withholding attack against Hemi's state transition function. If the full node does not have the correct full blocks to advance its indexers to the delayed tip behind the lightweight view, it waits for these blocks to become available.

The state of the full BTC node indexers must be identical across all Hemi nodes at a given L2 height so that hVM precompile calls are determinstic, otherwise nodes would calculate EVM state transitions incorrectly and cause a state divergence.

Bitcoin Attributes Deposited transactions can only be created by the Sequencer - similar to other System transactions ([Ethereum] Attributes Deposited and PoP Payout).

For now, a default starting Bitcoin testnet header is configured and will be used by default if not overridden, but the hVM Phase 0 activation height MUST be overridden.

This update also provides bug-fixes for existing precompiles which were activated on Hemi Testnet prior to this update which makes hVM state deterministic across the Hemi network.
  • Loading branch information
max-sanchez committed Jul 22, 2024
1 parent 83dd8a6 commit 6a4242c
Show file tree
Hide file tree
Showing 25 changed files with 3,208 additions and 398 deletions.
103 changes: 103 additions & 0 deletions cmd/geth/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,14 @@ import (
"bufio"
"errors"
"fmt"
"github.com/ethereum/go-ethereum/core/vm"
"github.com/hemilabs/heminetwork/cmd/btctool/bdf"
"github.com/hemilabs/heminetwork/service/tbc"
"os"
"reflect"
"runtime"
"strings"
"time"
"unicode"

"github.com/ethereum/go-ethereum/accounts"
Expand Down Expand Up @@ -194,8 +198,107 @@ func makeFullNode(ctx *cli.Context) (*node.Node, ethapi.Backend) {
cfg.Eth.OverrideVerkle = &v
}

if ctx.IsSet(utils.OverrideHvmEnabled.Name) {
v := ctx.Bool(utils.OverrideHvmEnabled.Name)
cfg.Eth.HvmEnabled = v
}
if ctx.IsSet(utils.OverrideHvmGenesisHeader.Name) {
v := ctx.String(utils.OverrideHvmGenesisHeader.Name)
cfg.Eth.HvmGenesisHeader = v
}
if ctx.IsSet(utils.OverrideHvmGenesisHeight.Name) {
v := ctx.Uint64(utils.OverrideHvmGenesisHeight.Name)
cfg.Eth.HvmGenesisHeight = v
}
if ctx.IsSet(utils.OverrideHvmHeaderDataDir.Name) {
v := ctx.String(utils.OverrideHvmHeaderDataDir.Name)
cfg.Eth.HvmHeaderDataDir = v
}
if ctx.IsSet(utils.OverrideHvm0.Name) {
v := ctx.Uint64(utils.OverrideHvm0.Name)
cfg.Eth.OverrideHemiHvm0 = &v
}

backend, eth := utils.RegisterEthService(stack, &cfg.Eth)

if cfg.Eth.HvmEnabled {
// Before starting up any other services, make sure TBC is in correct initial state
fullNodeTbcCfg := tbc.NewDefaultConfig()

// TODO: Pull from chain config, each Hemi chain should be configured with a corresponding BTC net
fullNodeTbcCfg.Network = "testnet3"

if ctx.IsSet(utils.TBCListenAddress.Name) {
fullNodeTbcCfg.ListenAddress = ctx.String(utils.TBCListenAddress.Name)
}
if ctx.IsSet(utils.TBCMaxCachedTxs.Name) {
fullNodeTbcCfg.MaxCachedTxs = ctx.Int(utils.TBCMaxCachedTxs.Name)
}
if ctx.IsSet(utils.TBCLevelDBHome.Name) {
fullNodeTbcCfg.LevelDBHome = ctx.String(utils.TBCLevelDBHome.Name)
}
if ctx.IsSet(utils.TBCBlockSanity.Name) {
fullNodeTbcCfg.BlockSanity = ctx.Bool(utils.TBCBlockSanity.Name)
}
if ctx.IsSet(utils.TBCNetwork.Name) {
fullNodeTbcCfg.Network = ctx.String(utils.TBCNetwork.Name)
}
if ctx.IsSet(utils.TBCPrometheusAddress.Name) {
fullNodeTbcCfg.PrometheusListenAddress = ctx.String(utils.TBCPrometheusAddress.Name)
}
if ctx.IsSet(utils.TBCSeeds.Name) {
fullNodeTbcCfg.Seeds = ctx.StringSlice(utils.TBCSeeds.Name)
}
// TODO: convert op-geth log level integer to TBC log level string

// Initialize TBC Bitcoin indexer to answer hVM queries
err := vm.SetupTBCFullNode(ctx.Context, fullNodeTbcCfg)
if err != nil {
log.Crit("Unable to setup TBC Full Node", "err", err)
}

// TODO: Review TBC Full-Node initial sync logic, maybe do a blocking call in contracts.go?
time.Sleep(5 * time.Second)
genesisHeader, err := bdf.Hex2Header(cfg.Eth.HvmGenesisHeader)
genesisHash := genesisHeader.BlockHash()
genesisHeight := cfg.Eth.HvmGenesisHeight
log.Info(fmt.Sprintf("TBC Full Node started, will sync to Bitcoin block %x configured as the start "+
"of hVM consensus tracking on this chain.", genesisHash[:]))
var syncInfo tbc.SyncInfo
for {
bh, bhb, err := vm.TBCFullNode.BlockHeaderBest(ctx.Context)
if err != nil {
log.Crit(fmt.Sprintf("could not get BlockHeaderBest: %v", err))
}

targetHash := bhb.BlockHash()
if bh > genesisHeight {
targetHash = genesisHash
}

if err := vm.TBCFullNode.SyncIndexersToHash(ctx.Context, &targetHash); err != nil {
log.Crit(fmt.Sprintf("could not sync TBC full node indexers to hash %x: %v", targetHash, err))
}

syncInfo = vm.TBCFullNode.Synced(ctx.Context)

log.Info(fmt.Sprintf("synced block headers to height %d, want to get to %d",
syncInfo.BlockHeader.Height, genesisHeight))
if syncInfo.BlockHeader.Height >= genesisHeight {
break
}

select {
case <-time.After(500 * time.Millisecond):
case <-ctx.Context.Done():
log.Crit("context done")
}

log.Info("TBC initial sync completed", "headerHeight", syncInfo.BlockHeader.Height,
"utxoIndexHeight", syncInfo.Utxo.Height, "txIndexHeight", syncInfo.Tx.Height)
}
}

// Create gauge with geth system and build information
if eth != nil { // The 'eth' backend may be nil in light mode
var protos []string
Expand Down
96 changes: 8 additions & 88 deletions cmd/geth/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,6 @@ import (
"strings"
"time"

"github.com/ethereum/go-ethereum/core/vm"
"github.com/hemilabs/heminetwork/service/tbc"

"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/accounts/keystore"
"github.com/ethereum/go-ethereum/cmd/utils"
Expand All @@ -52,8 +49,7 @@ import (
)

const (
clientIdentifier = "geth" // Client identifier to advertise over the network
defaultTbcInitHeight = 2585811
clientIdentifier = "geth" // Client identifier to advertise over the network
)

var (
Expand Down Expand Up @@ -165,7 +161,11 @@ var (
utils.TBCBlockSanity,
utils.TBCNetwork,
utils.TBCPrometheusAddress,
utils.TBCInitHeight,
utils.OverrideHvmEnabled,
utils.OverrideHvmGenesisHeader,
utils.OverrideHvmHeaderDataDir,
utils.OverrideHvmGenesisHeight,
utils.OverrideHvm0,
utils.TBCSeeds,
configFileFlag,
utils.LogDebugFlag,
Expand Down Expand Up @@ -369,6 +369,7 @@ func geth(ctx *cli.Context) error {
}

prepare(ctx)
// TODO MAX: Init 0
stack, backend := makeFullNode(ctx)
defer stack.Close()

Expand All @@ -381,88 +382,7 @@ func geth(ctx *cli.Context) error {
// it unlocks any requested accounts, and starts the RPC/IPC interfaces and the
// miner.
func startNode(ctx *cli.Context, stack *node.Node, backend ethapi.Backend, isConsole bool) {
// Before starting up any other services, make sure TBC is in correct initial state
tbcCfg := tbc.NewDefaultConfig()

// TODO: Pull from chain config, each Hemi chain should be configured with a corresponding BTC net
tbcCfg.Network = "testnet3"

if ctx.IsSet(utils.TBCListenAddress.Name) {
tbcCfg.ListenAddress = ctx.String(utils.TBCListenAddress.Name)
}
if ctx.IsSet(utils.TBCMaxCachedTxs.Name) {
tbcCfg.MaxCachedTxs = ctx.Int(utils.TBCMaxCachedTxs.Name)
}
if ctx.IsSet(utils.TBCLevelDBHome.Name) {
tbcCfg.LevelDBHome = ctx.String(utils.TBCLevelDBHome.Name)
}
if ctx.IsSet(utils.TBCBlockSanity.Name) {
tbcCfg.BlockSanity = ctx.Bool(utils.TBCBlockSanity.Name)
}
if ctx.IsSet(utils.TBCNetwork.Name) {
tbcCfg.Network = ctx.String(utils.TBCNetwork.Name)
}
if ctx.IsSet(utils.TBCPrometheusAddress.Name) {
tbcCfg.PrometheusListenAddress = ctx.String(utils.TBCPrometheusAddress.Name)
}
if ctx.IsSet(utils.TBCSeeds.Name) {
tbcCfg.Seeds = ctx.StringSlice(utils.TBCSeeds.Name)
}
// TODO: convert op-geth log level integer to TBC log level string

// Initialize TBC Bitcoin indexer to answer hVM queries
if err := vm.SetupTBC(ctx.Context, tbcCfg); err != nil {
log.Crit(fmt.Sprintf("could not SetupTBC: %v", err))
}

// TODO: Review, give TBC time to warm up
time.Sleep(5 * time.Second)

var initHeight uint64 = uint64(defaultTbcInitHeight)
if ctx.IsSet(utils.TBCInitHeight.Name) {
initHeight = ctx.Uint64(utils.TBCInitHeight.Name)
}

var syncInfo tbc.SyncInfo

for {
_, bhb, err := vm.TBCIndexer.BlockHeaderBest(ctx.Context)
if err != nil {
log.Crit(fmt.Sprintf("could not get BlockHeaderBest: %v", err))
}

bestHash := bhb.BlockHash()

if err := vm.TBCIndexer.SyncIndexersToHash(ctx.Context, &bestHash); err != nil {
log.Crit(fmt.Sprintf("could not SyncIndexersToHash: %v", err))
}

if err := vm.TBCIndexer.TxIndexer(ctx.Context, &bestHash); err != nil {
log.Crit(fmt.Sprintf("could not TxIndexer: %v", err))
}

if err := vm.TBCIndexer.UtxoIndexer(ctx.Context, &bestHash); err != nil {
log.Crit(fmt.Sprintf("could not UTXOIndexer: %v", err))
}

syncInfo = vm.TBCIndexer.Synced(ctx.Context)

log.Info(fmt.Sprintf("synced block headers to height %d, want to get to %d", syncInfo.BlockHeader.Height, initHeight))
if syncInfo.BlockHeader.Height >= initHeight {
break
}

select {
case <-time.After(500 * time.Millisecond):
case <-ctx.Context.Done():
log.Crit("context done")
}
}

log.Info("TBC initial sync completed", "headerHeight", syncInfo.BlockHeader.Height,
"utxoIndexHeight", syncInfo.Utxo.Height, "txIndexHeight", syncInfo.Tx.Height)

vm.SetInitReady()
// TODO MAX: TBC init taken from here

debug.Memsize.Add("node", stack)

Expand Down
36 changes: 30 additions & 6 deletions cmd/utils/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -933,18 +933,41 @@ var (
Category: flags.RollupCategory,
Value: "", // No Prometheus by default
}
TBCInitHeight = &cli.Uint64Flag{
Name: "tbc.initheight",
Usage: "Height to ensure tbc is at before starting geth",
Category: flags.RollupCategory,
Value: 2585811,
}
TBCSeeds = &cli.StringSliceFlag{
Name: "tbc.seeds",
Usage: "override tbc seeds when finding peers",
Category: flags.RollupCategory,
Value: nil,
}
OverrideHvmEnabled = &cli.BoolFlag{
Name: "hvm.enabled",
Usage: "override whether hVM is enabled",
Category: flags.RollupCategory,
Value: ethconfig.Defaults.HvmEnabled,
}
OverrideHvmGenesisHeader = &cli.StringFlag{
Name: "hvm.genesisheader",
Usage: "override the genesis block header where hVM starts tracking Bitcoin consensus",
Category: flags.RollupCategory,
Value: ethconfig.Defaults.HvmGenesisHeader,
}
OverrideHvmGenesisHeight = &cli.Uint64Flag{
Name: "hvm.genesisheight",
Usage: "override the genesis block height where hVM starts tracking Bitcoin consensus",
Category: flags.RollupCategory,
Value: ethconfig.Defaults.HvmGenesisHeight,
}
OverrideHvmHeaderDataDir = &cli.StringFlag{
Name: "hvm.headerdatadir",
Usage: "override the data directory where op-geth stores Bitcoin headers for hVM consensus tracking",
Category: flags.RollupCategory,
Value: ethconfig.Defaults.HvmHeaderDataDir,
}
OverrideHvm0 = &cli.Uint64Flag{
Name: "override.hvm0",
Usage: "Manually specify the hVM phase 0 activation timestamp, overriding the bundled setting",
Category: flags.EthCategory,
}

// Metrics flags
MetricsEnabledFlag = &cli.BoolFlag{
Expand Down Expand Up @@ -2033,6 +2056,7 @@ func SetDNSDiscoveryDefaults(cfg *ethconfig.Config, genesis common.Hash) {
// RegisterEthService adds an Ethereum client to the stack.
// The second return value is the full node instance.
func RegisterEthService(stack *node.Node, cfg *ethconfig.Config) (ethapi.Backend, *eth.Ethereum) {
// TODO MAX: Init 2
backend, err := eth.New(stack, cfg)
if err != nil {
Fatalf("Failed to register the Ethereum service: %v", err)
Expand Down
Loading

0 comments on commit 6a4242c

Please sign in to comment.