From 6a4242c9047f62efee52c19d06e7de98084c3701 Mon Sep 17 00:00:00 2001 From: Max Sanchez Date: Sun, 21 Jul 2024 23:43:25 -0400 Subject: [PATCH] hVM Phase 0: 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. --- cmd/geth/config.go | 103 ++ cmd/geth/main.go | 96 +- cmd/utils/flags.go | 36 +- core/blockchain.go | 1540 ++++++++++++++++- core/genesis.go | 6 + core/txpool/blobpool/blobpool.go | 4 + core/txpool/legacypool/legacypool.go | 4 + core/txpool/txpool.go | 5 + core/types/btc_attributes_deposit_data.go | 307 ++++ .../types/btc_attributes_deposit_data_test.go | 135 ++ core/types/btc_attributes_deposited_tx.go | 91 + core/types/transaction.go | 34 + core/vm/contracts.go | 1072 ++++++++---- core/vm/contracts_fuzz_test.go | 2 +- core/vm/contracts_test.go | 10 +- core/vm/evm.go | 7 + eth/backend.go | 28 + eth/ethconfig/config.go | 18 + eth/ethconfig/gen_config.go | 54 + go.mod | 2 +- go.sum | 2 - miner/payload_building.go | 1 + miner/worker.go | 35 +- params/config.go | 13 + params/protocol_params.go | 1 + 25 files changed, 3208 insertions(+), 398 deletions(-) create mode 100644 core/types/btc_attributes_deposit_data.go create mode 100644 core/types/btc_attributes_deposit_data_test.go create mode 100644 core/types/btc_attributes_deposited_tx.go diff --git a/cmd/geth/config.go b/cmd/geth/config.go index 7e5cf5edfb..e1fbe6960f 100644 --- a/cmd/geth/config.go +++ b/cmd/geth/config.go @@ -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" @@ -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 diff --git a/cmd/geth/main.go b/cmd/geth/main.go index cdb5892bb6..21696b65a7 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -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" @@ -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 ( @@ -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, @@ -369,6 +369,7 @@ func geth(ctx *cli.Context) error { } prepare(ctx) + // TODO MAX: Init 0 stack, backend := makeFullNode(ctx) defer stack.Close() @@ -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) diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index f2db0152a7..e5e9a516d4 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -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{ @@ -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) diff --git a/core/blockchain.go b/core/blockchain.go index 173639c86b..04fff2be3b 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -18,11 +18,17 @@ package core import ( + "bytes" context2 "context" "errors" "fmt" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" + "github.com/hemilabs/heminetwork/database/tbcd" + "github.com/hemilabs/heminetwork/service/tbc" "io" "math/big" + "os" "runtime" "strings" "sync" @@ -95,8 +101,25 @@ var ( errChainStopped = errors.New("blockchain is stopped") errInvalidOldChain = errors.New("invalid old chain") errInvalidNewChain = errors.New("invalid new chain") + + // The upstream ID used when TBC is in its genesis configuration for Hemi + hVMGenesisUpstreamId = [32]byte{ + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, + 0x48, 0x56, 0x4D, 0x47, 0x45, 0x4E, 0x45, 0x53, 0x49, 0x53, // HVMGENESIS + 0x48, 0x56, 0x4D, 0x47, 0x45, 0x4E, 0x45, 0x53, 0x49, 0x53, // HVMGENESIS + 0x06, 0x05, 0x04, 0x03, 0x02, 0x01} + + // Temporary dummy ID used when TBC is testing application of headers that will go into a new block + hVMDummyUpstreamId = [32]byte{ + 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, + 0x44, 0x55, 0x4D, 0x4D, 0x59, 0x42, 0x4C, 0x4F, 0x43, 0x4B, // DUMMYBLOCK + 0x44, 0x55, 0x4D, 0x4D, 0x59, 0x42, 0x4C, 0x4F, 0x43, 0x4B, // DUMMYBLOCK + 0x0C, 0x0B, 0x0A, 0x09, 0x08, 0x07} ) +// Used for communicating chain geometry when finding common ancestor between blocks for hVM state transition +type AncestorType int + const ( bodyCacheLimit = 256 blockCacheLimit = 256 @@ -130,6 +153,12 @@ const ( // The following incompatible database changes were added: // * New scheme for contract code in order to separate the codes and trie nodes BlockChainVersion uint64 = 8 + + // Number of blocks behind the lightweight TBC canonical tip that the full TBC node is indexed to. + // For example when a Bitcoin Attributes Deposited transaction adds headers 101 through 103, indexer + // would move from 98 to 101. + // TODO: Make this configurable as part of chain parameters? + hVMIndexerTipLag = 2 ) // CacheConfig contains the configuration values for the trie database @@ -260,6 +289,221 @@ type BlockChain struct { processor Processor // Block transaction processor interface forker *ForkChoice vmConfig vm.Config + + hvmEnabled bool + tbcHeaderNode *tbc.Server + tbcHeaderNodeConfig *tbc.Config + + // A temporary holding pen for blocks that are being considered but not yet + // written to disk to allow hVM consensus update functions to access these + // to extract the geometry changes they represent. + // TODO: consider refactor that allows these blocks to be passed directly + // into the hVM consensus update functions to make this easier to reason about. + tempBlocks map[string]*types.Block + tempHeaders map[string]*types.Header + + btcAttributesDepCacheBlockHash common.Hash + btcAttributesDepCacheEntry *types.BtcAttributesDepositedTx +} + +// getHeaderModeTBCEVMHeader returns the EVM header for which the +// header-only TBC node represents the cumulative Bitcoin state knowledge +func (bc *BlockChain) getHeaderModeTBCEVMHeader() (*types.Header, error) { + if !bc.hvmEnabled { + return nil, fmt.Errorf("getHeaderModeTBCEVMHeader() called but hVM is not enabled") + } + + stateId, err := bc.tbcHeaderNode.UpstreamStateId(context2.Background()) + if err != nil { + return nil, err + } + + // We are at genesis configuration, no error but no header represented yet + if bytes.Equal(stateId[:], hVMGenesisUpstreamId[:]) { + return nil, nil + } + + stateBlockHash := common.BytesToHash(stateId[:]) + header := bc.getHeaderFromDiskOrHoldingPen(stateBlockHash) + + if header != nil { + return header, nil + } + return nil, fmt.Errorf(fmt.Sprintf("Unable to find EVM header corresponding to hash %x", stateBlockHash[:])) +} + +// getHvmPhase0ActivationBlock descends the blockchain until it +// finds the first block which is after the hVM Phase 0 activation timestamp. +// TODO: cache this somewhere after calculating and make sure reorgs are considered to update cache +func (bc *BlockChain) getHvmPhase0ActivationBlock() (*types.Header, error) { + if !bc.hvmEnabled { + log.Warn("getHvmPhase0ActivationBlock called when hVM is disabled") + return nil, fmt.Errorf("hVM is disabled") + } + + cursor := bc.CurrentBlock() + + // Find the block where hVM Phase 0 activation occurs + // TODO: Make this more efficient with intelligent indexing based on timestamp + // instead of this simple descent. + // Note genesis block cannot contain a Bitcoin Attributes Deposited tx. + for cursor.Number.Uint64() > 1000 { + header := bc.GetHeaderByNumber(cursor.Number.Uint64() - 1000) + if !bc.chainConfig.IsHvm0(header.Time) { + // Our tip is now less than 1000 blocks above activation height, descend manually + break + } + + cursor = header + } + + // Walk back until we are either at genesis or we pass behind the hVM Phase 0 activation timestamp + for { + header := bc.GetHeaderByHash(cursor.ParentHash) + if bc.chainConfig.IsHvm0(header.Time) && header.Number.Uint64() > 0 { + cursor = header + continue + } + break + } + + return cursor, nil +} + +// performFullHvmStateRestore is used to clear and completely regenerate +// the embedded header-only TBC node from genesis state, applying all +// hVM header state transitions in all blocks up to the current configured +// EVM tip. +func (bc *BlockChain) performFullHvmHeaderStateRestore() { + if !bc.hvmEnabled { + log.Warn("performFullHvmHeaderStateRestore called but hVM is disabled") + return + } + + log.Info("*****************************************************************") + log.Info("Performing full hVM header state restore, this could take awhile.") + + bc.resetHvmHeaderNodeToGenesis() + + tip := bc.CurrentBlock() + + cursor, err := bc.getHvmPhase0ActivationBlock() + if err != nil { + log.Crit("Unable to get hVM Phase 0 activation block", "err", err) + } + + // Walk cursor forward until we get to our tip, assumes GetBlockByNumber correctly returns + // blocks on the canonical chain which will eventually reach the tip returned by bc.CurrentBlock() above + log.Info("Performing full hVM header state restore starting at block %s @ %d", + cursor.Hash().String(), cursor.Number.Uint64()) + + for { + // Print out progress so we know restore is progressing + if cursor.Number.Uint64()%1000 == 0 { + log.Info(fmt.Sprintf("Processing hVM header state changes for block %s @ %d", + cursor.Hash().String(), cursor.Number.Uint64())) + } + err := bc.applyHvmHeaderConsensusUpdate(cursor) + if err != nil { + log.Crit(fmt.Sprintf("Failed to fully restore hVM state, encountered an error processing hVM"+ + "state updates for block %s @ %d", cursor.Hash().String(), cursor.Number.Uint64()), "err", err) + } + if cursor.Number.Uint64() < tip.Number.Uint64() { + next := bc.GetHeaderByNumber(cursor.Number.Uint64() + 1) + if next != nil { + cursor = next + } else { + // next should never be nil because we are below tip + log.Crit(fmt.Sprintf("Reached unexpected end of chain while restoring hVM header state, "+ + "last header applied: %s @ %d", cursor.Hash().String(), cursor.Number.Uint64())) + } + } else { + break + } + } + log.Info(fmt.Sprintf("Done performing full hVM header state restore. Tip: %s @ %d", cursor.Hash().String(), + cursor.Number.Uint64())) +} + +// resetHvmHeaderNodeToGenesis is used in the event that chain corruption +// occurs either in the header-only TBC node specifically or in geth in general. +// This method deletes the entire header-only TBC node's data directory, +// and configures it with the effective genesis block defined in the config. +// If this is called to fix a header mode TBC corruption (rather than as part of +// a broader overall EVM reset to genesis), caller must also process all of +// the header state transitions defined by Bitcoin Attributes Deposited +// transactions in the current chain since the activation of hVM Phase 0. +// If this function fails to delete the data directory and restart external +// header mode TBC correctly, it fails with a critical error as we will be +// unable to properly process Hemi state transitions. +func (bc *BlockChain) resetHvmHeaderNodeToGenesis() { + log.Info("Resetting hVM header TBC node to genesis") + if bc.tbcHeaderNode != nil { + err := bc.tbcHeaderNode.ExternalHeaderTearDown() + if err != nil { + log.Crit("resetHvmHeaderNodeToGenesis failed when calling ExternalHeaderTearDown on TBC", "err", err) + } + } + + dataDir := bc.tbcHeaderNodeConfig.LevelDBHome + if err := os.RemoveAll(dataDir); err != nil { + log.Crit(fmt.Sprintf("ResetHvmHeaderNodeToGenesis unable to delete external header mode TBC "+ + "data directory %s", dataDir)) + } + log.Info("Deleted hVM header TBC node data directory", "dataDir", dataDir) + + bc.SetupHvmHeaderNode(bc.tbcHeaderNodeConfig) +} + +func (bc *BlockChain) SetupHvmHeaderNode(config *tbc.Config) { + if config.ExternalHeaderMode != true { + log.Crit("SetupHvmHeaderNode called with a TBC config that does not have ExternalHeaderMode set") + } + + tbcHeaderNode, err := tbc.NewServer(config) + if err != nil { + log.Crit("SetupHvmHeaderNode unable to create new TBC server", "err", err) + } + err = tbcHeaderNode.ExternalHeaderSetup(context2.Background()) + if err != nil { + log.Crit("SetupHvmHeaderNode unable to run ExternalHeaderSetup on TBC", "err", err) + } + + // Get the current state ID + stateId, err := tbcHeaderNode.UpstreamStateId(context2.Background()) + + potentialBlockHash := common.BytesToHash(stateId[:]) + potentialHeader := bc.GetHeaderByHash(potentialBlockHash) + if potentialHeader != nil { + // TBC has already been progressed with EVM blocks prior + log.Info("Setup hVM's header-only TBC node, it is currently at state representing block %s @ %d", + potentialHeader.Hash().String, potentialHeader.Number.Uint64()) + } else { + // TBC's stateId doesn't correspond to a known block. + // It is either in an invalid state, or it's in genesis configuration. + + _, bestHeader, err := tbcHeaderNode.BlockHeaderBest(context2.Background()) + if err != nil { + log.Crit("SetupHvmHeaderNode unable to get Best header on TBC", "err", err) + } + bestHeaderHash := bestHeader.BlockHash() + genesisHash := config.EffectiveGenesisBlock.BlockHash() + if bytes.Equal(bestHeaderHash[:], genesisHash[:]) { + // TBC is in genesis state, set its state id to the genesis id + err := tbcHeaderNode.SetUpstreamStateId(context2.Background(), &hVMGenesisUpstreamId) + if err != nil { + log.Crit("SetupHvmHeaderNode unable to set upstream state id", "err", err) + } + } else { + // TBC is in an invalid state + // TODO: Attempt a full restore automatically + log.Crit(fmt.Sprintf("Header-only TBC is in an invalid state on startup with a stateId of %x", stateId)) + } + } + + bc.tbcHeaderNode = tbcHeaderNode + bc.tbcHeaderNodeConfig = config + bc.hvmEnabled = true } // NewBlockChain returns a fully initialised block chain using information @@ -633,6 +877,1048 @@ func (bc *BlockChain) SetSafe(header *types.Header) { } } +// findCommonAncestor finds the common ancestor between two provided +// headers, or returns an error if it is unable to walk backwards the chain +// correctly. +// If either header is a direct parent of the other header, returns +// the parent header itself. +func (bc *BlockChain) findCommonAncestor(a *types.Header, b *types.Header) (*types.Header, error) { + // Set cursor to the higher of the two headers + highCursor := a + lowCursor := b + if b.Number.Uint64() > a.Number.Uint64() { + highCursor = b + lowCursor = a + } + + lowHeight := lowCursor.Number.Uint64() + + // Cursor is the higher header, walk it back to lowHeight + for i := highCursor.Number.Uint64(); i >= lowHeight; i-- { + highCursor = bc.GetHeader(highCursor.ParentHash, i-1) + } + + if highCursor.Hash().Cmp(lowCursor.Hash()) == 0 { + // If they are equal, then lowCursor is the ancestor + return lowCursor, nil + } + + if lowCursor.Number.Uint64() != highCursor.Number.Uint64() { + // Sanity check, should be impossible + log.Crit(fmt.Sprintf("when looking for common ancestor between %s @ %d and %s @ %d, "+ + "highCursor was walked back to height %d which doesn't match lowCursor height %d", + a.Hash().String(), a.Number.Uint64(), b.Hash().String(), b.Number.Uint64(), + highCursor.Number.Uint64(), lowCursor.Number.Uint64())) + } + + // While high and low cursors are not the same block, walk each back together block-by-block + for highCursor.Hash().Cmp(lowCursor.Hash()) != 0 { + // Walk each cursor back to their parent + highCursor = bc.GetHeader(highCursor.ParentHash, highCursor.Number.Uint64()-1) + lowCursor = bc.GetHeader(lowCursor.ParentHash, lowCursor.Number.Uint64()-1) + + if highCursor.Number.Uint64() == 0 { + return nil, fmt.Errorf("when looking for common ancestor between %s @ %d and %s @ %d, "+ + "we walked backwards to the genesis block without finding a common ancestor", + a.Hash().String(), a.Number.Uint64(), b.Hash().String(), b.Number.Uint64()) + } + } + + // high and low cursors match, found common ancestor + return highCursor, nil +} + +// unapplyHvmHeaderConsensusUpdate retrieves the block corresponding to +// the provided block header, extracts its Bitcoin Attributes Deposited +// transaction and, if it exists, removes the header information contained +// in it from the protocol's lightweight view of Bitcoin and verifies that +// TBC has been correctly returned to the canonical tip claimed by the +// previous block which contains a Bitcoin Attributes Deposited tx. +func (bc *BlockChain) unapplyHvmHeaderConsensusUpdate(header *types.Header) error { + block := bc.getBlockFromDiskOrHoldingPen(header.Hash()) + if block == nil { + return fmt.Errorf("unable to get block %s @ %d to unapply hVM consensus updates", + header.Hash().String(), header.Number.Uint64()) + } + + // When we unapply the current block, TBC's state will reflect that of the + // previous block + prevBlock := bc.getHeaderFromDiskOrHoldingPen(header.ParentHash) + stateTransitionTargetHash := [32]byte{} + + if bc.chainConfig.IsHvm0(header.Time) && !bc.chainConfig.IsHvm0(prevBlock.Time) { + // Special case, we are unapplying the hVM state transition for the activation block, + // so set the state transition target hash back to the genesis default + copy(stateTransitionTargetHash[0:32], hVMGenesisUpstreamId[0:32]) + } else { + // Previous block had hVM active + copy(stateTransitionTargetHash[0:32], prevBlock.Hash().Bytes()[0:32]) + } + + btcAttrDep, err := block.Transactions().ExtractBtcAttrData() + if err != nil { + // Error implies that state of Bitcoin Attributes Deposited tx in the transaction list is invalid. + // This should be impossible because any block which is being unapplied would have undergone the + // same check previously and passed. + // TODO: Bubble this error up and invalidate this previous block? + log.Crit(fmt.Sprintf("Error while extracting Bitcoin Attributes Deposited transaction to unwind "+ + "hVM state application for block %s @ %d", header.Hash().String(), header.Number.Uint64()), + "err", err) + } + + if btcAttrDep == nil { + log.Info(fmt.Sprintf("Nothing to unapply in hVM state for block %s @ %d; doesn't contain a Bitcoin "+ + "Attributes Deposited transaction", header.Hash().String(), header.Number.Uint64())) + + // There is no Bitcoin Attributes Deposited transaction in this block to unapply. + // Even though we didn't make any changes, explicitly update TBC's state id to indicate that + // TBC's current state is correct for previous after removing this block. The + // stateTransitionTargetHash is already set to the previous block or the genesis upsteam ID + // depending on whether previous parent had hVM Phase 0 active or not. + if bc.chainConfig.IsHvm0(header.Time) { + err := bc.tbcHeaderNode.SetUpstreamStateId(context2.Background(), &stateTransitionTargetHash) + if err != nil { + // TODO: Recovery mode that resets TBC header mode to genesis configuration and rebuilds it from hVM activation block + log.Crit(fmt.Sprintf("Error while updating the upstream state id in TBC with no corresponding "+ + "consensus state modifications for unapplying block %s @ %d", header.Hash().String(), + header.Number.Uint64()), "err", err) + } + } + return nil + } + + if !bc.chainConfig.IsHvm0(header.Time) { + // This should never happen, because the block shouldn't have a Bitcoin Attributes Deposited tx before this + // activation timestamp and already would have failed validation in the forward direction when originally + // applied + // TODO: Bubble this error up and invalidate this previous block? + log.Crit(fmt.Sprintf("block %s @ %d has a Bitcoin Attributes Deposited transaction but its timestamp "+ + "%d is before the hVM Phase 0 activation height %d", header.Hash().String(), header.Number.Uint64(), + header.Time, *bc.chainConfig.Hvm0Time)) + } + + currentTipHeight, currentTip, err := bc.tbcHeaderNode.BlockHeaderBest(context2.Background()) + if err != nil { + // This is a critical TBC failure, not related to block validity + // TODO: Recovery mode that resets TBC header mode to genesis configuration and rebuilds it from hVM activation block + log.Crit(fmt.Sprintf("when unapplying hVM changes for block %s @ %d, unable to retrieve tip "+ + "from lightweight TBC!", header.Hash().String(), header.Number.Uint64()), "err", err) + } + currentTipHash := currentTip.BlockHash() + + // Descend the Hemi chain from this height until either we find a block with a Bitcoin Attributes Deposited + // transaction or we get to before the hVM Phase 0 activation height to determine the correct previous + // tip. + // TODO: Get this state more efficiently from HvmState contract in EVM? + var expectedPreviousTipHash [32]byte + cursorNum := header.Number.Uint64() - 1 + cursor := bc.getBlockFromDiskOrHoldingPen(header.ParentHash) + for bc.chainConfig.IsHvm0(cursor.Time()) { + oldBtcAttrDep, err := cursor.Transactions().ExtractBtcAttrData() + if err != nil { + // Error implies that state of Bitcoin Attributes Deposited tx in the transaction list is invalid. + // This should be impossible because any block which is being unapplied would have undergone the + // same check previously and passed. + // TODO: Bubble this error up to invalidate the old block? + log.Crit(fmt.Sprintf("Error while extracting Bitcoin Attributes Deposited transaction from "+ + "prior block %s @ %d when attempting to unwind hVM state application for block %s @ %d", + cursor.Hash().String(), cursorNum, header.Hash().String(), header.Number.Uint64()), "err", err) + } + if oldBtcAttrDep != nil { + // Found previous state + expectedPreviousTipHash = oldBtcAttrDep.CanonicalTip + break + } + cursor = bc.getBlockFromDiskOrHoldingPen(cursor.ParentHash()) + } + if bytes.Equal(expectedPreviousTipHash[:], []byte{}) { + genHash := bc.tbcHeaderNodeConfig.EffectiveGenesisBlock.BlockHash() + copy(expectedPreviousTipHash[0:32], genHash[0:32]) + log.Info("when unapplying hVM changes for block %s @ %d, got to block %s @ %d with timestamp "+ + "%d which is before the hVM Phase 0 activation timestamp %d, so previous canonical tip should be "+ + "the genesis block %x", header.Hash().String(), header.Number.Uint64(), cursor.Hash().String(), + cursor.NumberU64(), cursor.Time, bc.chainConfig.Hvm0Time, genHash[:]) + } + + // Get the actual header represented by the previous canonical tip hash + expectedPreviousTipHashParsed, err := chainhash.NewHash(expectedPreviousTipHash[:]) + if err != nil { + log.Warn(fmt.Sprintf("Unable to create blockhash from %x", expectedPreviousTipHash[:])) + } + + expectedPreviousTip, expectedPreviousTipHeight, err := + bc.tbcHeaderNode.BlockHeaderByHash(context2.Background(), expectedPreviousTipHashParsed) + + if err != nil { + // This should never happen, it means TBC doesn't have a header which either: + // 1. Should have already been added to it when this older block was originally processed, or + // 2. Is the genesis block TBC is configured with + // TODO: TBC recovery from genesis + log.Crit(fmt.Sprintf("when unapplying hVM changes for block %s @ %d, previous canonical tip "+ + "should be %x but TBC encountered an error when fetching that header", header.Hash().String(), + header.Number.Uint64(), expectedPreviousTipHash[:])) + } + + // TODO: Better header to slice + var expectedPreviousTipBuf bytes.Buffer + err = expectedPreviousTip.Serialize(&expectedPreviousTipBuf) + if err != nil { + // This is a critical failure, not related to block validity + // TODO: TBC recovery from genesis + log.Crit(fmt.Sprintf("when unapplying hVM changes from block %s @ %d, unable to serialize "+ + "tip from lightweight TBC!", header.Hash().String(), header.Number.Uint64()), "err", err) + } + expectedPreviousTipBytes := [80]byte(expectedPreviousTipBuf.Bytes()) + + rt, lastHeader, err := bc.tbcHeaderNode.RemoveExternalHeaders( + context2.Background(), btcAttrDep.Headers, expectedPreviousTipBytes, &stateTransitionTargetHash) + if err != nil { + // This is a critical failure, not related to block validity + // TODO: TBC recovery from genesis + log.Crit(fmt.Sprintf("when unapplying hVM changes from block %s @ %d, unable to remove "+ + "%d headers and change the canonical tip from %s @ %d to %x @ %d", header.Hash().String(), + header.Number.Uint64(), len(btcAttrDep.Headers), currentTipHash.String(), currentTipHeight, + expectedPreviousTipHash[:], expectedPreviousTipHeight), "err", err) + } + lastHeaderHash := lastHeader.BlockHash() + + newHeight, newTip, err := bc.tbcHeaderNode.BlockHeaderBest(context2.Background()) + if err != nil { + // TODO: TBC recovery from genesis + log.Crit(fmt.Sprintf("when unapplying hVM changes from block %s @ %d, attempted to remove "+ + "%d headers and change the canonical tip from %s @ %d to %x @ %d, but TBC reports an error "+ + "getting the canonical tip after state transition", header.Hash().String(), + header.Number.Uint64(), len(btcAttrDep.Headers), currentTipHash.String(), currentTipHeight, + expectedPreviousTipHash[:], expectedPreviousTipHeight), "err", err) + } + + newTipHash := newTip.BlockHash() + + if !bytes.Equal(newTipHash[:], expectedPreviousTipHash[:]) { + // TODO: TBC recovery from genesis + log.Crit(fmt.Sprintf("when unapplying hVM changes from block %s @ %d, attempted to remove "+ + "%d headers and change the canonical tip from %s @ %d to %x @ %d, but TBC reports that the "+ + "canonical tip after state transition is %x @ %d which is incorrect", header.Hash().String(), + header.Number.Uint64(), len(btcAttrDep.Headers), currentTipHash.String(), currentTipHeight, + expectedPreviousTipHash[:], expectedPreviousTipHeight, newTipHash[:], newHeight), "err", err) + } + + log.Info(fmt.Sprintf("successfully unapplied hVM changes from block %s @ %d, removed %d headers "+ + "and changed the canonical tip from %s @ %d to %x @ %d, last header before removed chunk is %x, rt=%d", + header.Hash().String(), header.Number.Uint64(), len(btcAttrDep.Headers), currentTipHash.String(), + currentTipHeight, expectedPreviousTipHash[:], expectedPreviousTipHeight, lastHeaderHash[:], rt)) + + return nil +} + +func (bc *BlockChain) getBlockFromDiskOrHoldingPen(hash common.Hash) *types.Block { + block := bc.GetBlockByHash(hash) + if block == nil { + // Check the holding pen + block = bc.tempBlocks[hash.String()] + } + return block // Upstream must check if nil +} + +func (bc *BlockChain) getHeaderFromDiskOrHoldingPen(hash common.Hash) *types.Header { + header := bc.GetHeaderByHash(hash) + if header == nil { + // Check the holding pen + header = bc.tempHeaders[hash.String()] + } + return header // Upstream must check if nil +} + +// applyHvmHeaderConsensusUpdate retrieves the block corresponding to +// the provided block header, extracts its Bitcoin Attributes Deposited +// transaction and, if it exists, applies the headers contained in it +// to the protocol's lightweight view of Bitcoin and verifies that the +// claimed canonical tip is correct. +func (bc *BlockChain) applyHvmHeaderConsensusUpdate(header *types.Header) error { + block := bc.getBlockFromDiskOrHoldingPen(header.Hash()) + if block == nil { + // Block not on disk or in holding pen + return fmt.Errorf("unable to get block %s @ %d to apply hVM consensus updates", + header.Hash().String(), header.Number.Uint64()) + } + + stateTransitionTargetHash := [32]byte{} + copy(stateTransitionTargetHash[0:32], header.Hash().Bytes()[0:32]) + + // Store the current TBC state hash so we can put it back if we revert our changes here + previousStateTransitionHash, err := bc.tbcHeaderNode.UpstreamStateId(context2.Background()) + if err != nil { + log.Crit("Unable to get upstream state id from TBC", "err", err) + } else { + log.Info(fmt.Sprintf("Applying hVM header update: adding block %s @ %d, previous state id is %x", + header.Hash().String(), header.Number.Uint64(), previousStateTransitionHash[:])) + } + + prevHashSanity := common.BytesToHash(previousStateTransitionHash[:]) + if bytes.Equal(prevHashSanity[:], hVMGenesisUpstreamId[:]) { + log.Info(fmt.Sprintf("Applying first hVM header update on block %s @ %d", + header.Hash().String(), header.Number.Uint64())) + } else { + check := bc.getBlockFromDiskOrHoldingPen(prevHashSanity) + checkHash := check.Hash() + if !bytes.Equal(checkHash[:], header.ParentHash[:]) { + log.Crit(fmt.Sprintf("Applying hVM header update for block %s @ %d failed, "+ + "previous state id is %x but parent of updated block is %s @ %d", + header.Hash().String(), header.Number.Uint64(), previousStateTransitionHash[:], + header.ParentHash[:], header.Number.Uint64()-1)) + } + } + + btcAttrDep, err := block.Transactions().ExtractBtcAttrData() + if err != nil { + // Error implies that state of Bitcoin Attributes Deposited tx in the transaction list is invalid + // TODO: Bubble this error up to cause a block rejection instead + log.Crit(fmt.Sprintf("Error while extracting Bitcoin Attributes Deposited transaction to process hVM state "+ + "application for applying block %s @ %d", header.Hash().String(), header.Number.Uint64()), "err", err) + } + + if btcAttrDep == nil { + log.Info(fmt.Sprintf("Nothing to apply in hVM state for block %s @ %d; doesn't contain a Bitcoin "+ + "Attributes Deposited transaction", header.Hash().String(), header.Number.Uint64())) + + // Even though we didn't make any changes, explicitly update TBC's state id to indicate that + // TBC's current state is correct after processing this block if hVM Phase 0 is active at + // this block's timestamp + if bc.chainConfig.IsHvm0(header.Time) { + err := bc.tbcHeaderNode.SetUpstreamStateId(context2.Background(), &stateTransitionTargetHash) + if err != nil { + // TODO: Recovery mode that resets TBC header mode to genesis configuration and rebuilds it from hVM activation block + log.Crit(fmt.Sprintf("Error while updating the upstream state id in TBC with no corresponding "+ + "consensus state modifications for block %s @ %d", header.Hash().String(), header.Number.Uint64()), "err", err) + } + } + return nil + } + + if !bc.chainConfig.IsHvm0(header.Time) { // && btcAttrDep != nil per above check + // TODO: Bubble this error up to cause a block rejection instead + log.Crit(fmt.Sprintf("block %s @ %d has a Bitcoin Attributes Deposited transaction but its timestamp "+ + "%d is before the hVM Phase 0 activation height %d", header.Hash().String(), header.Number.Uint64(), + header.Time, *bc.chainConfig.Hvm0Time)) + } + + prevHeight, prevTip, err := bc.tbcHeaderNode.BlockHeaderBest(context2.Background()) + if err != nil { + // This is a critical TBC failure, not related to block validity + // TODO: Recovery mode that resets TBC header mode to genesis configuration and rebuilds it from hVM activation block + log.Crit(fmt.Sprintf("when processing block %s @ %d, unable to retrieve tip from lightweight TBC!", + header.Hash().String(), header.Number.Uint64()), "err", err) + } + + prevTipHash := prevTip.BlockHash() + + var prevTipBuf bytes.Buffer + err = prevTip.Serialize(&prevTipBuf) + if err != nil { + // This is a critical failure, not related to block validity + // TODO: Same recovery mode mentioned above + log.Crit(fmt.Sprintf("when processing block %s @ %d, unable to serialize tip from lightweight TBC!", + header.Hash().String(), header.Number.Uint64()), "err", err) + } + prevTipBytes := [80]byte(prevTipBuf.Bytes()) + + headersToAdd := len(btcAttrDep.Headers) + var lastHeader *[80]byte + if headersToAdd > 0 { + // BTC Attributes Deposited transaction communicates at least one new header, store the last one for reference + lastHeader = &btcAttrDep.Headers[headersToAdd-1] + + it, lbh, cbh, err := bc.tbcHeaderNode.AddExternalHeaders( + context2.Background(), btcAttrDep.Headers, &stateTransitionTargetHash) + if err != nil { + // TODO: Bubble this error up to cause a block rejection instead + log.Crit(fmt.Sprintf("block %s @ %d has a Bitcoin Attributes Deposited transaction which contains"+ + " %d Bitcoin headers, and adding these headers to the protocol's Bitcoin view caused an error", + header.Hash().String(), header.Number.Uint64(), len(btcAttrDep.Headers)), "err", err) + } + + cbHash := cbh.Hash[:] + // Check that the Bitcoin Attributes Deposited transaction claims the correct canonical tip + if !bytes.Equal(cbHash, btcAttrDep.CanonicalTip[:]) { + // Canonical tip determined by TBC based on the new headers does not match canonical tip claimed by + // Bitcoin Attributes Deposited transaction + // TODO: Bubble this error up to cause a block rejection instead + + // Print out error, then remove the bad headers to return TBC to the correct state + log.Error(fmt.Sprintf("block %s @ %d has a Bitcoin Attributes Deposited transaction which "+ + "claims that after adding %d headers ending with %x, the canonical tip should be %x, but after "+ + "adding the headers to TBC the canonical tip is %x", header.Hash().String(), header.Number.Uint64(), + headersToAdd, lastHeader[:], btcAttrDep.CanonicalTip[:], cbHash[:])) + + // Remove the added headers and set the canonical tip and previous upstream state id back to + // what it was prior to the invalid addition + rt, removalParent, err := bc.tbcHeaderNode.RemoveExternalHeaders( + context2.Background(), btcAttrDep.Headers, prevTipBytes, previousStateTransitionHash) + + if err != nil { + // TODO: Recovery + log.Crit(fmt.Sprintf("after adding headers ending with %x from the Bitcoin Attributes "+ + " Deposited transaction in block %s @ %d, unable to remove those headers from TBC's view", + lastHeader[:], header.Hash().String(), header.Number), "err", err) + } + + removalParentHash := removalParent.BlockHash() + + // TODO: Bubble this error up to cause a block rejection instead + log.Crit(fmt.Sprintf("successfully removed headers applied from invalid block %s @ %d, last header "+ + "before removed section is %x. Removal type: %d", header.Hash().String(), header.Number.Uint64(), + removalParentHash[:], rt)) + } + + lbHash := lbh.Hash[:] + if !bytes.Equal(lbHash, lastHeader[:]) { + // Indicates a bug in TBC, as TBC didn't add all the headers we passed in + log.Crit(fmt.Sprintf("block %s @ %d has a Bitcoin Attributes Deposited transaction which "+ + "contains %d headers ending in %x, but after adding those headers to lightweight TBC, TBC's last "+ + "added block was %x", header.Hash().String(), header.Number.Uint64(), headersToAdd, lastHeader[:], + lbHash[:])) + } + + log.Info(fmt.Sprintf("Successfully added %d bitcoin headers from the Bitcoin Attributes Deposited tx "+ + "from block %s @ %d, current canonical tip is %x, former tip was %x @ %d, insertType=%d", headersToAdd, + header.Hash().String(), header.Number.Uint64(), lbHash[:], prevTipHash[:], prevHeight, it)) + return nil + } else { + // No headers to add, make sure that claimed canonical in BTC Attributes Deposited matches TBC's current + if !bytes.Equal(prevTipHash[:], btcAttrDep.CanonicalTip[:]) { + // TODO: Bubble this error up to cause a block rejection instead + log.Crit(fmt.Sprintf("block %s @ %d contains a Bitcoin Attributes Deposited transaction which "+ + "does not contain any headers, but claims the canonical tip should be %x when light TBC's tip "+ + "is %x", header.Hash().String(), header.Number.Uint64(), btcAttrDep.CanonicalTip[:], prevTipHash[:])) + } + return nil + } + // Catch-all because we don't have returns after log.Crits which are going to be replaced with appropriate recovery action + return fmt.Errorf("unspecified error") +} + +func (bc *BlockChain) IsHvmEnabled() bool { + return bc.hvmEnabled +} + +// GetBitcoinAttributesForNextBlock generates a new Bitcoin Attributes Deposited transaction which +// should be added to the next EVM block. +func (bc *BlockChain) GetBitcoinAttributesForNextBlock(timestamp uint64) (*types.BtcAttributesDepositedTx, error) { + // Lock the chain mutex - all other code that modifies lightweight TBC node respects this mutex + // and locking this resource ensures that we can safely modify the lightweight TBC node to ensure + // the new the Bitcoin Attributes Deposited transaction we generate can be successfully applied + // when it occurs in a block for real, and also to ensure the canonical tip we report matches what + // canonical tip lightweight TBC will report after the specified headers are added. + if !bc.chainmu.TryLock() { + return nil, errChainStopped + } + defer bc.chainmu.Unlock() + + if !bc.hvmEnabled { + // hVM not enabled, nothing to do + return nil, nil + } + + if !bc.chainConfig.IsHvm0(timestamp) { + // hVM enabled but not yet at activation time, nothing to do + return nil, nil + } + + lastTip := bc.CurrentBlock() + if lastTip == nil { + log.Crit("Unable to generate the Bitcoin Attributes Deposited transaction, as the current EVM tip " + + "is unknown!") + } + lastTipHash := lastTip.Hash() + + log.Info(fmt.Sprintf("Generating Bitcoin Attributes Deposited transaction for a new block with timestamp "+ + "%d on top of prior block %s @ %d", timestamp, lastTip.Hash().String(), lastTip.Number.Uint64())) + + if bytes.Equal(bc.btcAttributesDepCacheBlockHash[:], lastTipHash[:]) { + if bc.btcAttributesDepCacheEntry != nil { + return bc.btcAttributesDepCacheEntry, nil + } + } + + // Sanity check: lightweight TBC node's state should always reflect lastTip when this is called. + // If it doesn't, log the error and manually move the lightweight node to represent the current + // tip so we can return valid data. + currentTbcEvmTip, err := bc.getHeaderModeTBCEVMHeader() + if err != nil { + log.Crit(fmt.Sprintf("Unable to get the EVM block that lightweight TBC's state represents "+ + "while trying to generate a Bitcoin Attributes Deposited transaction for the next block after "+ + "%s @ %d", lastTip.Hash().String(), lastTip.Number.Uint64()), "err", err) + } + if currentTbcEvmTip != nil { + if currentTbcEvmTip.Hash().Cmp(lastTip.Hash()) != 0 { + log.Error(fmt.Sprintf("When attempting to generate a Bitcoin Attributes Deposited transaction "+ + "for the next block after %s @ %d, lightweight TBC represents an incorrect EVM state of %s @ %d", + lastTip.Hash().String(), lastTip.Number.Uint64(), currentTbcEvmTip.Hash().String(), + currentTbcEvmTip.Number.Uint64())) + + // Attempting to generate Bitcoin Attributes Deposited transaction for the block after current tip + // but lightweight TBC's state isn't at the current tip, move it here manually + err := bc.updateHvmHeaderConsensus(lastTip) + if err != nil { + log.Crit(fmt.Sprintf("When attempting to generate a Bitcoin Attributes Deposited transaction "+ + "for the next block after %s @ %d, lightweight TBC represented an incorrect EVM state of %s @ %d "+ + "and an error occurred trying to move its EVM state.", + lastTip.Hash().String(), lastTip.Number.Uint64(), currentTbcEvmTip.Hash().String(), + currentTbcEvmTip.Number.Uint64())) + } + // TODO: decide whether to move TBC back to the "bad" (non-tip) state after this calculation. + } else { + log.Info(fmt.Sprintf("Lightweight TBC correctly represents block %s @ %d when attempting to "+ + "generate a Bitcoin Attributes Deposited transaction for the next block", + currentTbcEvmTip.Hash().String(), currentTbcEvmTip.Number.Uint64())) + } + } else { + log.Info(fmt.Sprintf("The EVM block corresponding to lightweight TBC's current state is nil, "+ + "which should indicate that the next block after %s @ %d at time %d is the hVM Phase 0 "+ + "activation block", lastTip.Hash().String(), lastTip.Number.Uint64(), timestamp)) + } + + originalTbcUpstreamId, err := bc.tbcHeaderNode.UpstreamStateId(context2.Background()) + if err != nil { + log.Crit(fmt.Sprintf("Unable to get the upstream state id from TBC when creating the Bitcoin "+ + "Attributes Deposited transaction for the block after %s @ %d ", lastTip.Hash().String(), + lastTip.Number.Uint64()), "err", err) + } + + // Get current tips known by our lightweight and full TBC nodes + lightTipHeight, lightTipHeader, err := bc.tbcHeaderNode.BlockHeaderBest(context2.Background()) + if err != nil { + log.Crit(fmt.Sprintf("Unable to get the best block header from lightweight TBC node when attempting "+ + "to calculate the Bitcoin Attributes Deposited transaction for next block after %s @ %d", + lastTip.Hash().String(), lastTip.Number.Uint64()), "err", err) + } + lightTipHash := lightTipHeader.BlockHash() + + fullTipHeight, fullTipHeader, err := vm.TBCFullNode.BlockHeaderBest(context2.Background()) + if err != nil { + log.Crit(fmt.Sprintf("Unable to get the best block header from TBC full node when attempting "+ + "to calculate the Bitcoin Attributes Deposited transaction for next block after %s @ %d", + lastTip.Hash().String(), lastTip.Number.Uint64()), "err", err) + } + fullTipHash := fullTipHeader.BlockHash() + + // Check whether the TBC Full Node has new header information compared to lightweight TBC node. + // Note this is looking at what block headers the TBC full node knows about, so is unrelated to + // where the full node is indexed to. + if bytes.Equal(lightTipHash[:], fullTipHash[:]) { + // Both TBC nodes have same consensus tip, nothing to do + return nil, nil + } + + // Tips are different - determine whether the lightweight tip is a direct ancestor. + // Note: we aren't using existing methods for finding common ancestor, because there is an + // edge case where lightweight consensus could know about a block header on a fork + // which the TBC full node is not aware of, so in the event of a fork we need to walk + // back each tip from their respective data source. This edge case could happen either + // in a benign way when there is a Bitcoin reorg and our TBC full node only heard about + // the canonical chain from peers, or if a malicious Sequencer intentionally privately + // mined a Bitcoin block and included the header in a Bitcoin Attributes Deposited tx + // in an attempt to cause an error in the hVM state transition. + lightCursorHeader := lightTipHeader + lightCursorHeight := lightTipHeight + lightCursorHash := lightTipHeader.BlockHash() + + fullCursorHeader := fullTipHeader + fullCursorHeight := fullTipHeight + fullCursorHash := fullTipHeader.BlockHash() + + log.Info(fmt.Sprintf("Generating Bitcoin Attributes Deposited transaction for the next block after "+ + "%s % %d, lightweight TBC node consensus tip is %s @ %d, full TBC node consensus tip is %s @ %d", + lastTip.Hash().String(), lastTip.Number.Uint64(), lightCursorHash[:], lightCursorHeight, + fullCursorHash[:], fullCursorHeight)) + + // Walk back the light cursor until we get to the same height if it's ahead + for lightCursorHeight > fullCursorHeight { + // Get height even though we could calculate it as a sanity check + header, height, err := bc.tbcHeaderNode.BlockHeaderByHash(context2.Background(), &lightCursorHeader.PrevBlock) + if err != nil { + // Should never happen, implies lightweight TBC has a header before its current + // canonical tip which it is unable to return, probably signals corruption. + // TODO: Lightweight TBC recovery from genesis + log.Crit(fmt.Sprintf("Unable to get header %x @ %d from lightweight TBC node when walking "+ + "backwards from %x @ %d", lightCursorHeader.PrevBlock[:], lightCursorHeight-1, + lightCursorHash[:], lightCursorHeight), "err", err) + } + if height != lightCursorHeight-1 { + // Should never happen, means lightweight TBC node is returning bad heights + log.Crit(fmt.Sprintf("Lightweight TBC node returned an incorrect height for block %x: "+ + "expected %d but got %d", lightCursorHeader.PrevBlock[:], lightCursorHeight-1, height)) + } + lightCursorHeader = header + lightCursorHeight = height // same as lightCursorHeight - 1 + lightCursorHash = lightCursorHeader.BlockHash() + } + // Walk back the full cursor until we get to the same height if it's ahead + for fullCursorHeight > lightCursorHeight { + // Get height even though we could calculate it as a sanity check + header, height, err := vm.TBCFullNode.BlockHeaderByHash(context2.Background(), &fullCursorHeader.PrevBlock) + if err != nil { + // Should never happen, implies full TBC node has a header before its current + // canonical tip which it is unable to return, probably signals corruption. + // TODO: Full TBC node recovery? + log.Crit(fmt.Sprintf("Unable to get header %x @ %d from full TBC node when walking "+ + "backwards from %x @ %d", fullCursorHeader.PrevBlock[:], fullCursorHeight-1, + fullCursorHash[:], fullCursorHeight), "err", err) + } + if height != fullCursorHeight-1 { + // Should never happen, means full TBC node is returning bad heights + log.Crit(fmt.Sprintf("Full TBC node returned an incorrect height for block %x: "+ + "expected %d but got %d", fullCursorHeader.PrevBlock[:], fullCursorHeight-1, height)) + } + fullCursorHeader = header + fullCursorHeight = height // same as fullCursorHeight - 1 + fullCursorHash = fullCursorHeader.BlockHash() + } + + // Whether or not lightweight and full TBC nodes are on the same chain, + // we will find their common ancestor (which will be the same as + // one of the node's tips if they are on the same chain) which we can + // use to only perform walk-back logic once rather than separately for + // the different forking scenarios. + var commonAncestorHash chainhash.Hash + + // Now the cursors for the lightweight and full node chains are at the same height. + // If both cursors match, both chains' current tips are on the same chain. + if bytes.Equal(fullCursorHash[:], lightCursorHash[:]) { + // They match, so they are on the same chain. + if lightTipHeight > fullTipHeight { + // Lightweight TBC has consensus ahead of full tip, on same chain, + // so nothing to do. + // + // We didn't check this until we got the block from both chains + // at the lowest height of either tip, because there is an edge + // edge case where lightweight tip could have been previously + // advanced onto a fork that is higher than canonical block known + // by the full node, but the full node still contains one or more + // headers on its canonical chain that should be communicated to + // the lightweight view. + return nil, nil + } else { + // Full TBC node has consensus ahead of lightweight tip on the + // same chain, so we just need to provide the new headers between + // lightweight TBC's tip and full TBC node's tip. + commonAncestorHash = lightTipHash + // Walk backwards from full TBC node's tip, adding all headers + // until we get to this common ancestor. + // We walk backwards from a known tip instead of advancing + // by index even though we know the tip we are walking towards + // is canonical to avoid an edge-case where the TBC full node + // could experience a reorg deeper than the common ancestor + // which would cause us to return headers which may not connect + // to the ancestor we know the lightweight TBC node will be able + // to progress on. + } + } else { + // Lightweight tip isn't the common ancestor, meaning the two nodes + // are on different chains. Need to continue walking both back + // until we do find a common ancestor. + // TODO: way to dedup this code with the previous walk-back to equal height, + // could move to a walkback function where caller can specify whether height + // or hash is used as exit condition and return all final cursors? + for !bytes.Equal(fullCursorHash[:], lightCursorHash[:]) { + lHeader, lHeight, err := bc.tbcHeaderNode.BlockHeaderByHash(context2.Background(), &lightCursorHeader.PrevBlock) + if err != nil { + // Should never happen, implies lightweight TBC has a header before its current + // canonical tip which it is unable to return, probably signals corruption. + // TODO: Lightweight TBC recovery from genesis + log.Crit(fmt.Sprintf("Unable to get header %x @ %d from lightweight TBC node when walking "+ + "backwards from %x @ %d", lightCursorHeader.PrevBlock[:], lightCursorHeight-1, + lightCursorHash[:], lightCursorHeight), "err", err) + } + if lHeight != lightCursorHeight-1 { + // Should never happen, means lightweight TBC node is returning bad heights + log.Crit(fmt.Sprintf("Lightweight TBC node returned an incorrect height for block %x: "+ + "expected %d but got %d", lightCursorHeader.PrevBlock[:], lightCursorHeight-1, lHeight)) + } + + fHeader, fHeight, err := vm.TBCFullNode.BlockHeaderByHash(context2.Background(), &fullCursorHeader.PrevBlock) + if err != nil { + // Should never happen, implies full TBC node has a header before its current + // canonical tip which it is unable to return, probably signals corruption. + // TODO: Full TBC node recovery? + log.Crit(fmt.Sprintf("Unable to get header %x @ %d from full TBC node when walking "+ + "backwards from %x @ %d", fullCursorHeader.PrevBlock[:], fullCursorHeight-1, + fullCursorHash[:], fullCursorHeight), "err", err) + } + if fHeight != fullCursorHeight-1 { + // Should never happen, means full TBC node is returning bad heights + log.Crit(fmt.Sprintf("Full TBC node returned an incorrect height for block %x: "+ + "expected %d but got %d", fullCursorHeader.PrevBlock[:], fullCursorHeight-1, fHeight)) + } + + lightCursorHeader = lHeader + lightCursorHeight = lHeight + lightCursorHash = lHeader.BlockHash() + + fullCursorHeader = fHeader + fullCursorHeight = fHeight + fullCursorHash = fHeader.BlockHash() + } + commonAncestorHash = fullCursorHash + } + + // Whether or not the light and full TBC nodes are on the same chain, we + // have their common ancestor so any headers from the TBC full node which + // connect to this ancestor are guaranteed to fit onto lightweight TBC's + // current knowledge. + commonAncestorHeight := fullCursorHeight // Both former cursors are ancestor now + + log.Info(fmt.Sprintf("When generating the Bitcoin Attributes Deposited transaction for the next block "+ + "after %s @ %d, the common ancestor between lightweight TBC tip %x @ %d and full node TBC tip %x @ %d is "+ + "%x @ %d", lastTip.Hash().String(), lastTip.Number.Uint64(), lightTipHash[:], lightTipHeight, + fullTipHash[:], fullTipHeight, commonAncestorHash[:], commonAncestorHeight)) + + // # of headers will always be the full tip minus the height of the common ancestor + var headersToTip []wire.BlockHeader + cursorHeight := fullTipHeight + cursorHeader := fullTipHeader + cursorHash := fullTipHash + + // Loop until cursor's hash matches the common ancestor + for !bytes.Equal(commonAncestorHash[:], cursorHash[:]) { + headersToTip = append(headersToTip, *cursorHeader) + tHeader, tHeight, err := vm.TBCFullNode.BlockHeaderByHash(context2.Background(), &cursorHeader.PrevBlock) + if err != nil { + // Should never happen, implies full TBC node has a header before its current + // canonical tip which it is unable to return, probably signals corruption. + // TODO: Full TBC node recovery? + log.Crit(fmt.Sprintf("Unable to get header %x @ %d from full TBC node when walking "+ + "backwards from %x @ %d", cursorHeader.PrevBlock[:], cursorHeight-1, + cursorHash[:], cursorHeight), "err", err) + } + if tHeight != cursorHeight-1 { + // Should never happen, means full TBC node is returning bad heights + log.Crit(fmt.Sprintf("Full TBC node returned an incorrect height for block %x: "+ + "expected %d but got %d", cursorHeader.PrevBlock[:], cursorHeight-1, tHeight)) + } + cursorHeader = tHeader + cursorHeight = tHeight + cursorHash = tHeader.BlockHash() + } + + if headersToTip == nil || len(headersToTip) == 0 { + // Sanity check just in case, this should never happen because the only way this array + // is empty should be if lightweight and full node tips are the same + log.Error(fmt.Sprintf("When generating Bitcoin Attributes Deposited transaction for block after "+ + "%s @ %d got past checks for whether any new headers are available from TBC full node that should be "+ + "communicated to TBC light mode, but did not find any headers to add. Common ancestor: %x", + lastTip.Hash().String(), lastTip.Number.Uint64(), commonAncestorHash[:])) + return nil, nil + } + + var headersToAdd []wire.BlockHeader + // Check that none of the headers we are going to add are already known by lightweight TBC. + // This is possible in an edge case where we are communicating a reorg, as lightweight + // TBC could know some blocks on the fork since the common ancestor which we didn't + // yet check for. Note headersToTip is in reverse order. + for index := len(headersToTip) - 1; index >= 0; index-- { + headerToAdd := headersToTip[index] + headerToAddHash := headerToAdd.BlockHash() + _, _, err := bc.tbcHeaderNode.BlockHeaderByHash(context2.Background(), &headerToAddHash) + if err != nil { // Error means header was not found + // TODO: Make sure the error is a NotFoundError, not some other failure + headersToAdd = append(headersToAdd, headerToAdd) + } + } + + // It's possible that all headers were already known by lightweight TBC if it is + // fully aware of the alternate chain in a chain-split scenario. + if len(headersToAdd) == 0 { + return nil, nil + } + + // Trim headersToAdd to the maximum number of headers we are allowed to include. + if len(headersToAdd) > types.MaximumBtcHeadersInTx { + headersToAdd = headersToAdd[0:types.MaximumBtcHeadersInTx] + } + + // Serialize headers to bytes + headersToAddSerialized, err := types.SerializeHeadersToArray(headersToAdd) + if err != nil { + log.Crit(fmt.Sprintf("Unable to serialize Bitcoin headers to create Bitcoin Attributes Deposited "+ + "transaction for the block after %s @ %d", lastTip.Hash().String(), lastTip.Number.Uint64()), "err", err) + } + + // Add the headers to lightweight TBC's view to make sure they are valid, and also to + // determine the correct new canonical tip (which won't be the last header in this array + // if we are adding knowledge to a fork that doesn't become canonical). That is possible + // if there is a split tip or if we are handling a BTC reorg that is more than + // MaximumBtcHeadersInTx deep and requires multiple Bitcoin Attributes Deposited transactions + // to communicate enough headers for it to be considered canonical by our lightweight view. + _, canonical, _, err := bc.tbcHeaderNode.AddExternalHeaders(context2.Background(), *headersToAddSerialized, &hVMDummyUpstreamId) + if err != nil { + first := headersToAdd[0].BlockHash() + last := headersToAdd[len(headersToAdd)-1].BlockHash() + log.Crit(fmt.Sprintf("Unable to add %d external headers %x to %x to lightweight TBC view on top "+ + "of prior canonical tip %x @ %d!", len(*headersToAddSerialized), first[:], last[:], lightTipHash, + lightTipHeight), "err", err) + return nil, err + } + + // Revert lightweight TBC's view back to what it was before we started. + priorTip, err := types.SerializeHeader(*lightTipHeader) + if err != nil { + log.Crit(fmt.Sprintf("Unable to serialize header for block %x while creating Bitcoin Attributes "+ + "Deposited transaction", lightTipHash[:]), "err", err) + } + rt, prevHeader, err := bc.tbcHeaderNode.RemoveExternalHeaders(context2.Background(), *headersToAddSerialized, *priorTip, originalTbcUpstreamId) + if err != nil { + first := headersToAdd[0].BlockHash() + last := headersToAdd[len(headersToAdd)-1].BlockHash() + log.Crit(fmt.Sprintf("Unable to remove %d external headers %x to %x from lightweight TBC view after "+ + "they were temporarily added when creating the Bitcoin Attributes Deposited transaction for the block "+ + "after %s @ %d", len(*headersToAddSerialized), first[:], last[:], lastTip.Hash().String(), + lastTip.Number.Uint64()), "err", err) + } + + log.Info(fmt.Sprintf("Successfully removed %d block headers from lightweight TBC view after temporarily "+ + "adding them when generating the Bitcoin Attributes Deposited transaction for the block after %s @ %d. "+ + "RemoveType=%d, prevHeader=%x", len(*headersToAddSerialized), lastTip.Hash().String(), lastTip.Number.Uint64(), + rt, prevHeader.Hash[:])) + + canonHashAfterDepTx := canonical.BlockHash() + btcAttrDepTx, err := types.MakeBtcAttributesDepositedTx(canonHashAfterDepTx, headersToAdd) + if err != nil { + log.Crit(fmt.Sprintf("Unable to construct a Bitcoin Attributes Deposited tx containing %d headers "+ + "with canonical hash %x for placement in the block after %s @ %d", len(headersToAdd), + canonHashAfterDepTx[:], lastTip.Hash().String(), lastTip.Number.Uint64()), "err", err) + } + + // Store the calculated Bitcoin Attributes Deposited transaction so we don't need to recalculate + // it on subsequent calls to build on top of the same parent. + bc.btcAttributesDepCacheBlockHash = lastTip.Hash() + bc.btcAttributesDepCacheEntry = btcAttrDepTx + + return btcAttrDepTx, nil +} + +// headersBetweenBlocks returns an array of headers from ancestor (inclusive) to head (inclusive). +// This function requires that ancestor is an ancestor of head; if the ancestor cannot be found by +// walking backwards from the head an error will be thrown. +// This function does not depend on canonical indexes, so it can safely be used to find the route +// to walk forward from an ancestor to its descendant whether or not some or all of the headers +// on the route are canonical, as long as all of the block headers exist in the database. +// Headers are returned in ascending order: [ancestor, ..., head] +func (bc *BlockChain) headersBetweenBlocks(ancestor *types.Header, head *types.Header) ([]*types.Header, error) { + if ancestor == nil { + return nil, fmt.Errorf("headersBetweenBlocks called with nil ancestor") + } + if head == nil { + return nil, fmt.Errorf("headersBetweenBlocks called with nil head") + } + + headIndex := head.Number.Uint64() + ancestorIndex := ancestor.Number.Uint64() + path := make([]*types.Header, headIndex-ancestorIndex+1) + + cursor := head + path[headIndex-ancestorIndex] = cursor + for index := headIndex - ancestorIndex - 1; index >= 0; index-- { + // Don't overwrite cursor so we can print error correctly + cursorTmp := bc.getHeaderFromDiskOrHoldingPen(cursor.ParentHash) + if cursorTmp == nil { + return nil, fmt.Errorf(fmt.Sprintf("headersBetweenBlocks could not retrieve header %s @ %d", + cursor.ParentHash.String(), cursor.Number.Uint64()-1)) + } + path[index] = cursorTmp + cursor = cursorTmp + } + + return path, nil +} + +func (bc *BlockChain) walkHvmHeaderConsensusForward(currentHead *types.Header, newHead *types.Header) error { + // Can't walk forwards from a block that is the same height or higher than the destination + if currentHead.Number.Uint64() >= newHead.Number.Uint64() { + return fmt.Errorf(fmt.Sprintf("Cannot walk hVM consensus forewards from "+ + "%s @ %d to %s @ %d - bad geometry", currentHead.Hash().String(), currentHead.Number.Uint64(), + newHead.Hash().String(), newHead.Number.Uint64())) + } + + // It may be unsafe to walk forwards by number in case this method is called + // before the appropriate canonical chain is fully updated in the database + // (meaning walking forward could return blocks that aren't between the + // current and new head), so walk backwards from newHead until we get to + // currentHead, and then walk forwards through the list. + headers, err := bc.headersBetweenBlocks(currentHead, newHead) + if err != nil { + // Critical error, this indicates that a path between the block responsible + // for hVM's latest state update and the new head we are setting cannot + // be found. + // TODO: Attempt to recover hVM state from genesis + return fmt.Errorf("unable to find a path between hVM's latest state update block %s @ %d and "+ + "the new head %s @ %d", currentHead.Hash().String(), currentHead.Number.Uint64(), + newHead.Hash().String(), newHead.Number.Uint64()) + } + + // Start at 1 to skip the currentHead which has been processed previously + for index := 1; index < len(headers); index++ { + err := bc.applyHvmHeaderConsensusUpdate(headers[index]) + if err != nil { + // TODO: Invalidate the failing block OR attempt to recover hVM state from genesis + return fmt.Errorf("unable to apply the hVM header state transition for block %s @ %d, err: %v", + headers[index].Hash().String(), headers[index].Number.Uint64(), err) + } + } + + return nil +} + +func (bc *BlockChain) walkHvmHeaderConsensusBack(currentHead *types.Header, newHead *types.Header) error { + // Can't walk backwards from a block that is the same height or lower than the destination + if currentHead.Number.Uint64() <= newHead.Number.Uint64() { + return fmt.Errorf(fmt.Sprintf("Cannot walk hVM consensus backwards from "+ + "%s @ %d to %s @ %d - bad geometry", currentHead.Hash().String(), currentHead.Number.Uint64(), + newHead.Hash().String(), newHead.Number.Uint64())) + } + + log.Info(fmt.Sprintf("walkHvmHeaderConsensusBack called to walk backwards from %s @ %d to %s @ %d", + currentHead.Hash().String(), currentHead.Number.Uint64(), newHead.Hash().String(), newHead.Number.Uint64())) + + cursor := currentHead + // Loop walking back the cursor until the cursor points to the newHead, since + // newHead is the ancestor and once we unapply the hVM state transition from + // newHead's direct child TBC's state will be reverted to the appropriate state. + for cursor.Hash().Cmp(newHead.Hash()) != 0 { + if cursor.Number.Uint64() == newHead.Number.Uint64() { + // Should be impossible, this indicates that newHead is not actually + // a direct ancestor of currentHead and our common ancestor is incorrect + return fmt.Errorf("walking backwards from block %s @ %d, reached block %s @ %d but "+ + "was expecting the block at index %d to be %s which is the new head we are unwinding to", + currentHead.Hash().String(), currentHead.Number.Uint64(), cursor.Hash().String(), + cursor.Number.Uint64(), cursor.Number.Uint64(), newHead.Hash().String()) + } + + err := bc.unapplyHvmHeaderConsensusUpdate(cursor) + if err != nil { + log.Crit(fmt.Sprintf("Unable to unapply the hVM header %s @ %d", + cursor.Hash().String(), cursor.Number.Uint64()), "err", err) + } + newCursor := bc.getHeaderFromDiskOrHoldingPen(cursor.ParentHash) + if newCursor == nil { + log.Crit(fmt.Sprintf("Unable to get header for block %s @ %d", + cursor.ParentHash.String(), cursor.Number.Uint64()-1)) + } + cursor = newCursor + } + + // We expect hVM to have an upstream state id corresponding to newHead, sanity check it + upstreamStateId, err := bc.tbcHeaderNode.UpstreamStateId(context2.Background()) + if err != nil { + return err + } + if !bytes.Equal(upstreamStateId[:], newHead.Hash().Bytes()[:]) { + return fmt.Errorf("after walking backwards from block %s @ %d to %s @ %d, expected TBC "+ + "upstream state id to be %s but got %x instead", currentHead.Hash().String(), currentHead.Number.Uint64(), + newHead.Hash().String(), newHead.Number.Uint64(), newHead.Hash().String(), upstreamStateId[:]) + } + + return nil +} + +// updateHvmHeaderConsensus must be called each time when the canonical +// tip is changed. This method determines the change in chain geometry +// that the switch to the new block represents, and modifies the +// external-header-mode TBC instance's Bitcoin header knowledge to +// account for only information contained in the canonical chain ending +// at the new head. +func (bc *BlockChain) updateHvmHeaderConsensus(newHead *types.Header) error { + if !bc.hvmEnabled { + log.Warn("updateHvmHeaderConsensus called but hVM is disabled") + return nil + } + + log.Info(fmt.Sprintf("updateHvmHeaderConsensus called with new head: %s @ %d", + newHead.Hash().String(), newHead.Number.Uint64())) + + // We store the EVM block which was last applied to update hVM + // independently in order to gracefully handle updates to EVM + // blockchain state that occurred without TBC's knowledge. + // In the future, this may also be used for some kind of + // snap sync of TBC state or similar. + currentHeadHashRaw, err := bc.tbcHeaderNode.UpstreamStateId(context2.Background()) + log.Info(fmt.Sprintf("current upstream state id from TBC is %x", currentHeadHashRaw[:])) + + if bytes.Equal(currentHeadHashRaw[:], tbcd.DefaultUpstreamStateId[:]) { + // TODO: Full TBC recovery from genesis + log.Crit("hVM's header-only TBC node reported a default upstream state id, " + + "indicating it was modified without a proper corresponding EVM block to identify its state.") + } + + if bytes.Equal(currentHeadHashRaw[:], newHead.Hash().Bytes()[:]) { + log.Info(fmt.Sprintf("updateHvmHeaderConsensus called to update chain to new head %x but lightweight "+ + "TBC node's state already reflects this block, no-op", currentHeadHashRaw[:])) + return nil + } + + currentHeadHash := common.BytesToHash(currentHeadHashRaw[:]) + currentHead := bc.GetHeaderByHash(currentHeadHash) + + // Get common ancestor between newHead and currentHead + ancestor, err := bc.findCommonAncestor(newHead, currentHead) + if err != nil || ancestor == nil { + log.Crit(fmt.Sprintf("Unable to find common ancestor between %s @ %d and %s @ %d,"+ + " cannot transition hVM's header knowledge to the correct state", + newHead.Hash().String(), newHead.Number.Uint64(), + currentHead.Hash().String(), currentHead.Number.Uint64()), "err", err) + } + + log.Info(fmt.Sprintf("Common ancestor between %s @ %d and %s @ %d is %s @ %d", + currentHead.Hash().String(), currentHead.Number.Uint64(), newHead.Hash().String(), + newHead.Number.Uint64(), ancestor.Hash().String(), ancestor.Number.Uint64())) + + // If currentHead is direct parent, then just apply state change from newHead + if newHead.ParentHash.Cmp(ancestor.Hash()) == 0 { + err := bc.applyHvmHeaderConsensusUpdate(newHead) + if err != nil { + // TODO: This is where we should invalidate the block OR attempt to recover hVM state from genesis + // depending on the nature of the error. For now, crit. + log.Crit(fmt.Sprintf("Encountered an error applying hVM header state transition for block %s @ %d", + newHead.Hash().String(), newHead.Number.Uint64()), "err", err) + } + log.Info(fmt.Sprintf("Successfully applied hVM header state transition for single block %s @ %d")) + return nil + } else if bytes.Equal(currentHead.Hash().Bytes(), ancestor.Hash().Bytes()) { + // If currentHead is the ancestor, then we are walking directly forwards. + err := bc.walkHvmHeaderConsensusForward(currentHead, newHead) + if err != nil { + // TODO: depending on error either recover hVM state from genesis or mark blocks invalid + log.Crit("Unable to walk hVM consensus forwards", "err", err) + } + + } else if bytes.Equal(newHead.Hash().Bytes(), ancestor.Hash().Bytes()) { + // Otherwise if newHead is the ancestor, then we are walking directly backwards. + err := bc.walkHvmHeaderConsensusBack(currentHead, newHead) + if err != nil { + // TODO: depending on error either recover hVM state from genesis or mark blocks invalid + log.Crit("Unable to walk hVM consensus backwards", "err", err) + } + } else { + // Finally if neither newHead or currentHead is the ancestor, then we are in a fork and need to walk + // backwards from currentHead until we reach ancestor, then forward to newHead. + + // First, walk backwards from currentHead to common ancestor + err := bc.walkHvmHeaderConsensusBack(currentHead, ancestor) + if err != nil { + // TODO: depending on error either recover hVM state from genesis or mark blocks invalid + log.Crit("Unable to walk hVM consensus backwards", "err", err) + } + // Then, walk forwards from the common ancestor + err = bc.walkHvmHeaderConsensusForward(ancestor, newHead) + if err != nil { + // TODO: depending on error either recover hVM state from genesis or mark blocks invalid + log.Crit("Unable to walk hVM consensus backwards", "err", err) + } + } + + return nil +} + // setHeadBeyondRoot rewinds the local chain to a new head with the extra condition // that the rewind must pass the specified state root. This method is meant to be // used when rewinding with snapshots enabled to ensure that we go back further than @@ -717,6 +2003,14 @@ func (bc *BlockChain) setHeadBeyondRoot(head uint64, time uint64, root common.Ha bc.currentBlock.Store(newHeadBlock.Header()) headBlockGauge.Update(int64(newHeadBlock.NumberU64())) + log.Info(fmt.Sprintf("Updating hVM header consensus in setHeadBeyondRoot updateFn to %s @ %d", + newHeadBlock.Header().Hash(), newHeadBlock.Number().Uint64())) + err := bc.updateHvmHeaderConsensus(newHeadBlock.Header()) + if err != nil { + log.Crit(fmt.Sprintf("Unable to udpate hVM header consensus in setHeadBeyondRoot updateFn to %s @ %d", + newHeadBlock.Header().Hash(), newHeadBlock.Number().Uint64()), "err", err) + } + // The head state is missing, which is only possible in the path-based // scheme. This situation occurs when the chain head is rewound below // the pivot point. In this scenario, there is no possible recovery @@ -877,6 +2171,7 @@ func (bc *BlockChain) ResetWithGenesisBlock(genesis *types.Block) error { // Last update all in-memory chain markers bc.genesisBlock = genesis bc.currentBlock.Store(bc.genesisBlock.Header()) + bc.resetHvmHeaderNodeToGenesis() // No need to restore as we're resetting EVM state to genesis too headBlockGauge.Update(int64(bc.genesisBlock.NumberU64())) bc.hc.SetGenesis(bc.genesisBlock.Header()) bc.hc.SetCurrentHeader(bc.genesisBlock.Header()) @@ -949,6 +2244,14 @@ func (bc *BlockChain) writeHeadBlock(block *types.Block) { bc.currentBlock.Store(block.Header()) headBlockGauge.Update(int64(block.NumberU64())) + + log.Info("Updating hVM header consensus to block %s @ %d in writeHeadBlock()", + block.Hash().String(), block.Number().Uint64()) + err := bc.updateHvmHeaderConsensus(block.Header()) + if err != nil { + log.Crit("Unable to update hVM header consensus to block %s @ %d in writeHeadBlock()", + block.Hash().String(), block.Number().Uint64(), "err", err) + } } // stopWithoutSaving stops the blockchain service. If any imports are currently in progress @@ -1470,6 +2773,13 @@ func (bc *BlockChain) writeBlockAndSetHead(block *types.Block, receipts []*types // Set new head. if status == CanonStatTy { bc.writeHeadBlock(block) + log.Info("Updating hVM header consensus to block %s @ %d in writeBlockAndSetHead()", + block.Hash().String(), block.Number().Uint64()) + err := bc.updateHvmHeaderConsensus(block.Header()) + if err != nil { + log.Crit("Unable to update hVM header consensus to block %s @ %d in writeBlockAndSetHead()", + block.Hash().String(), block.Number().Uint64(), "err", err) + } } bc.futureBlocks.Remove(block.Hash()) @@ -1684,7 +2994,16 @@ func (bc *BlockChain) insertChain(chain types.Blocks, setHead bool) (int, error) } }() + // When this function is over, clear all temp blocks + defer clear(bc.tempBlocks) + defer clear(bc.tempHeaders) + for ; block != nil && err == nil || errors.Is(err, ErrKnownBlock); block, err = it.next() { + // Add this block to temporary holding pen so hVM consensus update functions have access + // to it. + bc.tempBlocks[block.Hash().String()] = block + bc.tempHeaders[block.Hash().String()] = block.Header() + // If the chain is terminating, stop processing blocks if bc.insertStopped() { log.Debug("Abort during block processing") @@ -1770,12 +3089,199 @@ func (bc *BlockChain) insertChain(chain types.Blocks, setHead bool) (int, error) // Process block using the parent state as reference point pstart := time.Now() + // TODO: Evaluate scenarios where very old blocks could require significant work to walk TBC correctly to validate + // TODO: if full-node TBC progression fails because it doesn't know of block yet, queue this EVM block for processing later + // Before processing a block: + // 1. Check whether header-only TBC node's state is at this block's parent; if it's not move it + // here temporarily and store the former state to restore to once we're finished processing + // 2. Apply this block's Bitcoin Attributes Deposited transaction to header-only TBC node's state + // (If this results in an error, report/invalidate the block same as an invalid EVM state transition) + // 3. Update the full TBC node's indexed tip to be 2 blocks behind the header-only TBC node's tip + // (If this results in an error, report/invalidate the block same as an invalid EVM state transition) + // + // Then after processing a block: + // 1. If block processing fails or setHead is false, walk header-only TBC node's state to former restore state + // Otherwise, leave header-only TBC in progressed state with this block as tip + // 2. If we walk header-only TBC node's state back, then walk back TBC full node's indexed tip to be 2 blocks + // behind the header-only TBC node's tip after the restore + + var tbcHeader *types.Header // Original EVM tip that lightweight TBC knowledge represents to revert to when necessary + var indexedState *tbc.SyncInfo // Original state of TBC Full Node to revert to when necessary + isHvmActivated := false + isFirstHvmBlock := false + if bc.hvmEnabled { + var parent *types.Header + + if bc.chainConfig.IsHvm0(block.Time()) { + log.Info(fmt.Sprintf("For block %s @ %d, hVM is activated", + block.Hash().String(), block.NumberU64())) + isHvmActivated = true + indexedState = vm.GetTBCFullNodeSyncStatus() + if block.NumberU64() != 0 { + parent = bc.GetHeaderByHash(block.ParentHash()) + if !bc.chainConfig.IsHvm0(parent.Time) { + // Parent is not hVM0, meaning this block is first activation + log.Info(fmt.Sprintf("Block %s @ %d is the hVM activation block", + block.Hash().String(), block.NumberU64())) + isFirstHvmBlock = true + } + } else { + // Genesis is first hVM block + isFirstHvmBlock = true + log.Info(fmt.Sprintf("Genesis block %s @ %d is the hVM activation block", + block.Hash().String(), block.NumberU64())) + } + } + + if isHvmActivated { + if !isFirstHvmBlock { + // Store current state of lightweight TBC to restore to later if necessary + tbcHeader, err = bc.getHeaderModeTBCEVMHeader() + if err != nil { + log.Crit("Error encountered getting EVM block lightweight TBC's state represents", "err", err) + } + } // else: tbcHeader will remain nil, check later to know to revert to TBC genesis state rather than state based on EVM block + } + + if tbcHeader != nil { + log.Info(fmt.Sprintf("Processing block %s @ %d at timestamp %d, TBC state header is %s @ %d", + block.Hash().String(), block.Number().Uint64(), block.Time(), tbcHeader.Hash().String(), + tbcHeader.Number.Uint64())) + } else if isHvmActivated { + log.Info(fmt.Sprintf("Processing block %s @ %d at timestamp %d, this is the first hVM state "+ + "transition block", block.Hash().String(), block.Number().Uint64(), block.Time())) + } + + // First, move lightweight TBC state to parent if this block is not the hVM Phase 0 activation block. + // The full TBC node *doesn't* need any intermediate hop to parent consensus, since it's only to provide + // linear indexed state based on a particular Bitcoin tip which is dictated by the lightweight TBC node. + // The lightweight TBC node *does* need to be adjusted to a specific pre-state based on this block's + // parent to ensure that this block communicates data which is correct in the context of it's parent, + // otherwise different nodes could disagree on the validity of this block's Bitcoin Attributes Deposited tx + // for lightweight consensus update based on different lightweight Bitcoin views. + // The updateHvmHeaderConsensus() method does handle underlying reorganizations of TBC's EVM state + // including reversing down a fork and up a new branch to the EVM header we specify, but moving the + // geometry here gives us more control here to know why an error occurred and in the future use various + // recovery mechanisms depending on the issue. + if !isFirstHvmBlock { + if tbcHeader.Hash().Cmp(parent.Hash()) != 0 { + log.Info(fmt.Sprintf("Lightweight TBC at block %s @ %d, moving to parent %s @ %d", + tbcHeader.Hash().String(), tbcHeader.Number.Uint64(), + parent.Hash().String(), parent.Number.Uint64())) + err := bc.updateHvmHeaderConsensus(parent) + if err != nil { + log.Crit(fmt.Sprintf("Unable to move lightweight TBC node to parent %s @ %d", + parent.Hash().String(), parent.Number.Uint64()), "err", err) + } + } else { + log.Info(fmt.Sprintf("Lightweight TBC is already at parent %s @ %d", + parent.Hash().String(), parent.Number.Uint64())) + } + } + + // Do an extra sanity check that lightweight TBC node is in the correct state. + // Incorrect state represents either data corruption or an issue with the reorg logic. + // TODO on incorrect state attempt automated recovery of lightweight TBC state instead of log.Crit() exit + if !isFirstHvmBlock { + log.Info(fmt.Sprintf("Verifying before applying block %s @ %d, lightweight TBC's state is "+ + "correctly set to direct parent %s @ %d", block.Hash().String(), block.NumberU64(), + parent.Hash().String(), parent.Number.Uint64())) + representedBlock, err := bc.getHeaderModeTBCEVMHeader() + if err != nil { + log.Crit(fmt.Sprintf("Error, unable to fetch the EVM tip which lightweight TBC state "+ + "currently represents!"), "err", err) + } + if representedBlock.Hash().Cmp(parent.Hash()) != 0 { + stateId, err := bc.tbcHeaderNode.UpstreamStateId(context2.Background()) + if err != nil { + // Should never happen since UpstreamStateId is called by getHeaderModeTBCEVMHeader() too + log.Crit(fmt.Sprintf("Error, lightweight TBC state represents unexpected EVM tip "+ + "%s @ %d, and we encountered an error fetching the upstream state id!", + representedBlock.Hash().String(), representedBlock.Number.Uint64()), "err", err) + } + log.Crit(fmt.Sprintf("Error, lightweight TBC state represents unexpected EVM tip %s @ %d"+ + " with upstream state id %x instead", representedBlock.Hash().String(), + representedBlock.Number.Uint64(), stateId[:])) + } + } + + // Process this block's hVM updates + err := bc.updateHvmHeaderConsensus(block.Header()) + if err != nil { + // TODO: ban block instead + log.Crit(fmt.Sprintf("TBC lightweight node unable to update lightweight TBC node to block "+ + "to process %s @ %d", block.Header().Hash().String(), block.Header().Number.Uint64())) + } + + // Update TBC Full Node's indexing to represent lightweight view minus 2 BTC blocks + lightTipHeight, lightTipHeader, err := bc.tbcHeaderNode.BlockHeaderBest(context2.Background()) + if err != nil { + log.Crit(fmt.Sprintf("when processing block %s @ %d, an error occurred getting lightweight"+ + " TBC's best block", block.Hash().String(), block.NumberU64())) + } + lightTipHash := lightTipHeader.BlockHash() + + cursorHeight, cursorHeader := lightTipHeight, lightTipHeader + cursorHash := cursorHeader.BlockHash() + log.Info("Lightweight TBC is at canonical BTC block %s @ %d", cursorHash[:], cursorHeight) + // walk back hVMIndexerTipLag blocks from tip + // On initial init when we have less than hVMIndexerTipLag previous blocks (right after + // hVM0 phase transition), correct indexer behavior is to remain at the genesis-configured + // height until walking backwards the specified number of lag blocks doesn't surpass + // configured genesis. + if cursorHeight-hVMIndexerTipLag > bc.tbcHeaderNodeConfig.GenesisHeightOffset { + for i := 0; i < hVMIndexerTipLag; i++ { + head, height, err := bc.tbcHeaderNode.BlockHeaderByHash(context2.Background(), &cursorHeader.PrevBlock) + if err != nil { + log.Crit(fmt.Sprintf("when processing block %s @ %d, an error occurred walking back "+ + "Bitcoin headers from lightweight TBC tip %x @ %d, unable to get header %x @ %d", + block.Hash().String(), block.NumberU64(), lightTipHash[:], lightTipHeight, + cursorHeader.PrevBlock[:], cursorHeight-1)) + } + cursorHeader, cursorHeight = head, height // Storing them temporarily for verbose logging + cursorHash = cursorHeader.BlockHash() + } + } + + // Check that the TBC Full Node has sufficient chain knowledge to sync to this height. + // TODO: More intelligent handling of this in the future - adding to queue and processing later + // rather than busy-waiting here + available, err := vm.TBCBlocksAvailableToHeader(context2.Background(), cursorHeader) + if err != nil { + log.Crit(fmt.Sprintf("when processing block %s @ %d, got an unexpected error determining if "+ + "TBC Full Node hash blocks available to index up to Bitcoin block %x", block.Hash().String(), + block.NumberU64(), cursorHash[:])) + } + for !available { + available, err = vm.TBCBlocksAvailableToHeader(context2.Background(), cursorHeader) + if err != nil { + log.Crit(fmt.Sprintf("when processing block %s @ %d, got an unexpected error determining if "+ + "TBC Full Node hash blocks available to index up to Bitcoin block %x", block.Hash().String(), + block.NumberU64(), cursorHash[:])) + } + log.Info("when processing block %s @ %d, TBC Full Node does not yet have all full blocks "+ + "up to Bitcoin block %x which is required to progress EVM state, waiting...", block.Hash().String(), + block.NumberU64(), cursorHash[:]) + time.Sleep(1 * time.Second) + } + + // This single indexer function handles any reorgs required to move the TBC full node to the specified index + err = vm.TBCIndexToHeader(cursorHeader) + if err != nil { + // TODO: Recovery? + log.Crit(fmt.Sprintf("Unable to move TBC Full Node indexes to BTC block %x @ %d", + cursorHash[:], cursorHeight), "err", err) + } + } + receipts, logs, usedGas, err := bc.processor.Process(block, statedb, bc.vmConfig) if err != nil { bc.reportBlock(block, receipts, err) followupInterrupt.Store(true) return it.index, err } + log.Info(fmt.Sprintf("Performed EVM processing of block %s @ %d", + block.Hash().String(), block.NumberU64())) ptime := time.Since(pstart) vstart := time.Now() @@ -1810,8 +3316,33 @@ func (bc *BlockChain) insertChain(chain types.Blocks, setHead bool) (int, error) ) if !setHead { // Don't set the head, only insert the block + log.Info(fmt.Sprintf("Writing block %s @ %d to disk but not setting as head.", block.Hash().String(), + block.NumberU64())) + // Because this block is not canonical, revert lightweight and full TBC nodes to former state + if tbcHeader != nil { + err := bc.updateHvmHeaderConsensus(tbcHeader) + if err != nil { + // TODO: Recover lightweight TBC state from genesis + log.Crit(fmt.Sprintf("Unable to revert lightweight TBC node to represent state at "+ + "block %s @ %d.", tbcHeader.Hash().String(), tbcHeader.Number.Uint64()), "err", err) + } else { + log.Info(fmt.Sprintf("Successfully reverted lightweight TBC node to represent state at "+ + "block %s @ %d.", tbcHeader.Hash().String(), tbcHeader.Number.Uint64())) + } + } + if indexedState != nil { + err := vm.TBCRestoreIndexersToPoint(indexedState) + if err != nil { + // TODO: Recovery of TBC full node? + log.Crit(fmt.Sprintf("Unable to restore TBC full node to previous indexed state "+ + "of UTXO Indexer=%x@%d, Tx Indexer=%x@%d", indexedState.Utxo.Hash[:], indexedState.Utxo.Height, + indexedState.Tx.Hash[:], indexedState.Tx.Height)) + } + } err = bc.writeBlockWithState(block, receipts, statedb) } else { + log.Info(fmt.Sprintf("Writing block %s @ %d to disk and setting as head, leaving lightweight and "+ + "full node TBC states progressed", block.Hash().String(), block.NumberU64())) status, err = bc.writeBlockAndSetHead(block, receipts, logs, statedb, false) } followupInterrupt.Store(true) @@ -2310,6 +3841,14 @@ func (bc *BlockChain) SetCanonical(head *types.Block) (common.Hash, error) { } bc.writeHeadBlock(head) + log.Info("Updating hVM header consensus to block %s @ %d in SetCanonical()", + head.Hash().String(), head.Number().Uint64()) + err := bc.updateHvmHeaderConsensus(head.Header()) + if err != nil { + log.Crit("Unable to update hVM header consensus to block %s @ %d in SetCanonical()", + head.Hash().String(), head.Number().Uint64(), "err", err) + } + // Emit events logs := bc.collectLogs(head, false) bc.chainFeed.Send(ChainEvent{Block: head, Hash: head.Hash(), Logs: logs}) @@ -2328,7 +3867,6 @@ func (bc *BlockChain) SetCanonical(head *types.Block) (common.Hash, error) { context = append(context, []interface{}{"age", common.PrettyAge(timestamp)}...) } log.Info("Chain head was updated", context...) - vm.ProgressTip(context2.Background(), uint32(head.Time())) return head.Hash(), nil } diff --git a/core/genesis.go b/core/genesis.go index 839f10e179..e63cc2f60b 100644 --- a/core/genesis.go +++ b/core/genesis.go @@ -273,6 +273,9 @@ type ChainOverrides struct { OverrideOptimismEcotone *uint64 ApplySuperchainUpgrades bool OverrideOptimismInterop *uint64 + + // Hemi + OverrideHemiHvm0 *uint64 } // SetupGenesisBlock writes or updates the genesis block in db. @@ -339,6 +342,9 @@ func SetupGenesisBlockWithOverride(db ethdb.Database, triedb *trie.Database, gen if overrides != nil && overrides.OverrideOptimismInterop != nil { config.InteropTime = overrides.OverrideOptimismInterop } + if overrides != nil && overrides.OverrideHemiHvm0 != nil { + config.Hvm0Time = overrides.OverrideHemiHvm0 + } } } // Just commit the new block if there is no stored genesis block. diff --git a/core/txpool/blobpool/blobpool.go b/core/txpool/blobpool/blobpool.go index cb224e8d68..b5265d6e9d 100644 --- a/core/txpool/blobpool/blobpool.go +++ b/core/txpool/blobpool/blobpool.go @@ -1168,6 +1168,10 @@ func (p *BlobPool) Add(txs []*types.Transaction, local bool, sync bool) []error errs = make([]error, len(txs)) ) for i, tx := range txs { + if tx.IsBtcAttributesDepositedTx() || tx.IsPopPayoutTx() { + // Should never happen with blobs, but extra protection + continue + } errs[i] = p.add(tx) if errs[i] == nil { adds = append(adds, tx.WithoutBlobTxSidecar()) diff --git a/core/txpool/legacypool/legacypool.go b/core/txpool/legacypool/legacypool.go index 430ed3aa1c..127e3b3d02 100644 --- a/core/txpool/legacypool/legacypool.go +++ b/core/txpool/legacypool/legacypool.go @@ -1000,6 +1000,10 @@ func (pool *LegacyPool) Add(txs []*types.Transaction, local, sync bool) []error news = make([]*types.Transaction, 0, len(txs)) ) for i, tx := range txs { + if tx.IsBtcAttributesDepositedTx() || tx.IsPopPayoutTx() { + // Should never happen, but extra protection + continue + } // If the transaction is known, pre-set the error slot if pool.all.Get(tx.Hash()) != nil { errs[i] = txpool.ErrAlreadyKnown diff --git a/core/txpool/txpool.go b/core/txpool/txpool.go index 12361bba45..b52f3d7de6 100644 --- a/core/txpool/txpool.go +++ b/core/txpool/txpool.go @@ -321,6 +321,11 @@ func (p *TxPool) Add(txs []*types.Transaction, local bool, sync bool) []error { splits := make([]int, len(txs)) for i, tx := range txs { + if tx.IsBtcAttributesDepositedTx() || tx.IsPopPayoutTx() { + // Should never happen, but extra protection + continue + } + // Mark this transaction belonging to no-subpool splits[i] = -1 diff --git a/core/types/btc_attributes_deposit_data.go b/core/types/btc_attributes_deposit_data.go new file mode 100644 index 0000000000..9a6f3d90e4 --- /dev/null +++ b/core/types/btc_attributes_deposit_data.go @@ -0,0 +1,307 @@ +package types + +import ( + "bytes" + "encoding/binary" + "fmt" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/log" + "io" +) + +const ( + BtcAttributesDepositedFuncSignature = "updateHvmState(bytes32,bytes[])" + MaximumBtcHeadersInTx = 6 + SmartContractArgumentByteLen = 32 // Each argument to smart contract is padded to 32 bytes + + // Based on function sig + canonical tip + empty byte[] (pos + 0 len = 64) + MinimumSerializedBtcAttributesDepositedLen = 4 + 32 + (2 * 32) + + HeaderArrayOffset = 0x40 // 2 * 32 bytes + BitcoinHeaderLengthBytes = 0x50 // 80 bytes + BitcoinHashLengthBytes = 0x20 // 32 bytes +) + +var ( + UpdateHvmStateFuncBytes4 = crypto.Keccak256([]byte(BtcAttributesDepositedFuncSignature))[:4] + uint64EmptyPadding = [SmartContractArgumentByteLen - 8]byte{} + btcHeaderPadding = [SmartContractArgumentByteLen*3 - BitcoinHeaderLengthBytes]byte{} // To store 80 bytes we need to pad to closest higher multiple of 32 (which is 96) + HvmStateAddress = common.HexToAddress("0x8400000000000000000000000000000000000000") // Must match optimism repo "predeploys.HvmStateAddr" +) + +type BtcAttributesDepositData struct { + CanonicalTip [BitcoinHashLengthBytes]byte + Headers [][BitcoinHeaderLengthBytes]byte +} + +func calculateLength(numHeaders int) int { + return 4 + + SmartContractArgumentByteLen + // canonical tip hash + SmartContractArgumentByteLen + // offset of byte array + SmartContractArgumentByteLen + // length of byte array + (SmartContractArgumentByteLen * numHeaders) + // 1 for each header offset + (SmartContractArgumentByteLen * numHeaders) + // 1 for each header length + (SmartContractArgumentByteLen * numHeaders * 3) // 3 for each header's actual data +} + +// MarshalBinary Binary Format +// +---------+-------------------------------+ +// | Bytes | Field | +// +---------+-------------------------------+ +// | 4 | Function Signature | +// | 32 | Canonical Tip Hash | +// | 32 | Starting Pos of Header Arr | +// | 32 | Header Arr Length | +// | 32 | Starting Pos of 1st Header | +// | ... | +// | 32 | Starting Pos of last Header | +// | 32 | Length of 1st Header (0x50) | +// | 96 | 1st Header (80 padded to 96) | +// | ... | +// | 32 | Length of Last Header (0x50) | +// | 96 | Last Header (80 padded to 96) | +// +---------+-------------------------------+ +// +// Example (where 1122...7788 is a 32-byte hash, and "aaa...aaa", "bbb...bbb", and "ccc...ccc" are 80-byte headers: +// 0xc94f1cca // keccak256("updateHvmState(bytes32,bytes[])") +// 1122334455667788112233445566778811223344556677881122334455667788 // Canonical tip hash +// 0000000000000000000000000000000000000000000000000000000000000040 // Header array always starts 64 bytes in (next line) +// 0000000000000000000000000000000000000000000000000000000000000003 // Number of headers in array (3) +// 0000000000000000000000000000000000000000000000000000000000000060 // 1st header starts 96 bytes from here (in 3 lines) +// 00000000000000000000000000000000000000000000000000000000000000e0 // 2nd header starts 224 bytes from here (in 7 lines) +// 0000000000000000000000000000000000000000000000000000000000000160 // 3rd header starts 352 bytes from here (in 11 lines) +// 0000000000000000000000000000000000000000000000000000000000000050 // 1st header is length 80 bytes +// aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa // first 32 bytes of header +// aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa // second 32 bytes of header +// aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa00000000000000000000000000000000 // final 16 bytes of header + 16 bytes of padding +// 0000000000000000000000000000000000000000000000000000000000000050 // 2nd header is length 80 bytes +// bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb // first 32 bytes of header +// bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb // second 32 bytes of header +// bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb00000000000000000000000000000000 // final 16 bytes of header + 16 bytes of padding +// 0000000000000000000000000000000000000000000000000000000000000050 // 3rd header is length 80 bytes +// cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc // first 32 bytes of header +// cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc // second 32 bytes of header +// cccccccccccccccccccccccccccccccc00000000000000000000000000000000 // final 16 bytes of header + 16 bytes of padding +// +// Example with no headers: +// 0xc94f1cca // keccak256("updateHvmState(bytes32,bytes[])") +// 1122334455667788112233445566778811223344556677881122334455667788 // Canonical tip hash +// 0000000000000000000000000000000000000000000000000000000000000040 // Header array always starts 64 bytes in (even if empty) +// 0000000000000000000000000000000000000000000000000000000000000000 // Header array has 0 elements, done +func (btcdep *BtcAttributesDepositData) MarshalBinary() ([]byte, error) { + // See above format for calculation, assumes addresses and amounts are always same length + btcAttrDepLen := calculateLength(len(btcdep.Headers)) + + w := bytes.NewBuffer(make([]byte, 0, btcAttrDepLen)) + w.Write(UpdateHvmStateFuncBytes4[:]) + w.Write(btcdep.CanonicalTip[:]) + + // Offset of byte array is always 64 bytes in + w.Write(uint64EmptyPadding[:]) + binary.Write(w, binary.BigEndian, uint64(HeaderArrayOffset)) + + // Write number of elements in byte array + w.Write(uint64EmptyPadding[:]) + binary.Write(w, binary.BigEndian, uint64(len(btcdep.Headers))) + + if len(btcdep.Headers) == 0 { + // Done + return w.Bytes(), nil + } + + // Each header moves the start of the first header by 32 bytes + offset := SmartContractArgumentByteLen * len(btcdep.Headers) + for i := 0; i < len(btcdep.Headers); i++ { + w.Write(uint64EmptyPadding[:]) + binary.Write(w, binary.BigEndian, uint64(offset)) + // Each header takes 128 bytes (80 padded to 96, and another 32 for length) + offset += SmartContractArgumentByteLen * 4 + } + + for i := 0; i < len(btcdep.Headers); i++ { + w.Write(uint64EmptyPadding[:]) + binary.Write(w, binary.BigEndian, uint64(BitcoinHeaderLengthBytes)) // Header length is always 80 + w.Write(btcdep.Headers[i][:]) + w.Write(btcHeaderPadding[:]) + } + + return w.Bytes(), nil +} + +func (btcdep *BtcAttributesDepositData) UnmarshalBinary(data []byte) error { + if len(data) < MinimumSerializedBtcAttributesDepositedLen { + return fmt.Errorf("serialized Bitcoin Attributes Deposited data must be at least %d bytes,"+ + " but only %d bytes provided", MinimumSerializedBtcAttributesDepositedLen, len(data)) + } + + reader := bytes.NewReader(data) + sig := make([]byte, 4) + if _, err := io.ReadFull(reader, sig); err != nil { + return err + } + if !bytes.Equal(sig, UpdateHvmStateFuncBytes4[:]) { + return fmt.Errorf("serialized Bitcoin Attributes Deposited data must have a "+ + "function signature of 0x%x, but got 0x%x instead", UpdateHvmStateFuncBytes4, sig) + } + + var canonicalTip [BitcoinHashLengthBytes]byte + if _, err := io.ReadFull(reader, canonicalTip[:]); err != nil { + return err + } + + initialOffset, err := ReadUint64(reader) + if err != nil { + return err + } + if initialOffset != HeaderArrayOffset { + return fmt.Errorf("serialized Bitcoin Attributes Deposited data must have an "+ + "initial offset for the header array of %d, but got %d instead", HeaderArrayOffset, initialOffset) + } + + numHeaders, err := ReadUint64(reader) + if err != nil { + return err + } + + if numHeaders > MaximumBtcHeadersInTx { + return fmt.Errorf("serialized Bitcoin Attributes Deposited data can only have "+ + "a maximum of %d BTC headers, but got %d", MaximumBtcHeadersInTx, numHeaders) + } + + expectedOffset := SmartContractArgumentByteLen * int(numHeaders) + for i := 0; i < int(numHeaders); i++ { + actualOffset, err := ReadUint64(reader) + + if err != nil { + return err + } + + if expectedOffset != int(actualOffset) { + return fmt.Errorf("serialized Bitcoin Attributes Deposited data with %d headers "+ + "should have an offset of %d for the array at index %d, but got %d instead", + numHeaders, expectedOffset, i, actualOffset) + } + expectedOffset += SmartContractArgumentByteLen * 4 + } + + headers := make([][BitcoinHeaderLengthBytes]byte, numHeaders) + for i := 0; i < int(numHeaders); i++ { + headerLength, err := ReadUint64(reader) + if err != nil { + return err + } + + if headerLength != BitcoinHeaderLengthBytes { + return fmt.Errorf("serialized Bitcoin Attributes Deposited data should have "+ + "Bitcoin headers that are exactly %d bytes long, but the Bitcoin header at index %d "+ + "is encoded with a length of %d instead", BitcoinHeaderLengthBytes, i, headerLength) + } + + header, err := ReadBitcoinHeader(reader) + if err != nil { + return err + } + headers[i] = *header + } + + if !EmptyReader(reader) { + return fmt.Errorf("serialized Bitcoin Attributes Deposited data has more data than "+ + "expected! Expected size %d, but got %d", len(data), calculateLength(int(numHeaders))) + } + + btcdep.CanonicalTip = canonicalTip + btcdep.Headers = headers + + return nil +} + +func MakeBtcAttributesDepositedTx(canonicalTip *chainhash.Hash, headers []wire.BlockHeader) (*BtcAttributesDepositedTx, error) { + headersFlat, err := SerializeHeadersToArray(headers) + + btcAttrDepData := BtcAttributesDepositData{ + CanonicalTip: *canonicalTip, + Headers: *headersFlat, + } + + data, err := btcAttrDepData.MarshalBinary() + if err != nil { + log.Error("Unable to marshall binary for Bitcoin attributes data") + return nil, err + } + + out := &BtcAttributesDepositedTx{ + To: &BtcAttributesDepositedSenderAddress, + Gas: 1_000_000, // Regloith System Tx Gas + Data: data, + } + + return out, nil +} + +func SerializeHeader(header wire.BlockHeader) (*[BitcoinHeaderLengthBytes]byte, error) { + var headerBuf bytes.Buffer + err := header.Serialize(&headerBuf) + if err != nil { + hash := header.BlockHash() + log.Error(fmt.Sprintf("Unable to serialize header %x", hash[:])) + return nil, err + } + headerBytes := [BitcoinHeaderLengthBytes]byte(headerBuf.Bytes()) + return &headerBytes, nil +} + +func SerializeHeadersToArray(headers []wire.BlockHeader) (*[][BitcoinHeaderLengthBytes]byte, error) { + headersFlat := make([][BitcoinHeaderLengthBytes]byte, len(headers)) + for i := 0; i < len(headers); i++ { + header, err := SerializeHeader(headers[i]) + if err != nil { + hash := headers[i].BlockHash() + log.Error(fmt.Sprintf("Unable to serialize header %x when creating a Bitcoin Attributes "+ + "Deposited transaction", hash[:])) + } + headersFlat[i] = *header + } + + return &headersFlat, nil +} + +// From Solabi +func EmptyReader(r io.Reader) bool { + var t [1]byte + n, err := r.Read(t[:]) + return n == 0 && err == io.EOF +} + +// From Solabi +func ReadUint64(r io.Reader) (uint64, error) { + var readPadding [SmartContractArgumentByteLen - 8]byte + var n uint64 + if _, err := io.ReadFull(r, readPadding[:]); err != nil { + return n, err + } else if !bytes.Equal(readPadding[:], uint64EmptyPadding[:]) { + return n, fmt.Errorf("number padding was not empty: %x", readPadding[:]) + } + if err := binary.Read(r, binary.BigEndian, &n); err != nil { + return 0, fmt.Errorf("expected number length to be 8 bytes") + } + return n, nil +} + +// TODO: Add this new "ReadBitcoinHeader" upstream into solabi in our optimism fork and import here for use +func ReadBitcoinHeader(r io.Reader) (*[BitcoinHeaderLengthBytes]byte, error) { + var header [BitcoinHeaderLengthBytes]byte + if _, err := io.ReadFull(r, header[:]); err != nil { + return nil, err + } + var afterPadding [3*SmartContractArgumentByteLen - BitcoinHeaderLengthBytes]byte + if _, err := io.ReadFull(r, afterPadding[:]); err != nil { + return nil, err + } + if !bytes.Equal(afterPadding[:], btcHeaderPadding[:]) { + return nil, fmt.Errorf("header padding was not empty: %x", afterPadding[:]) + } + return &header, nil +} diff --git a/core/types/btc_attributes_deposit_data_test.go b/core/types/btc_attributes_deposit_data_test.go new file mode 100644 index 0000000000..529fcf4a61 --- /dev/null +++ b/core/types/btc_attributes_deposit_data_test.go @@ -0,0 +1,135 @@ +package types + +import ( + "bytes" + "github.com/ethereum/go-ethereum/common/hexutil" + "testing" +) + +func TestBtcAttributesDepositDataNoHeaders(t *testing.T) { + hash, _ := hexutil.Decode("0x1122334455667788112233445566778811223344556677881122334455667788") + + headers := make([][80]byte, 0) + + btcDepData := BtcAttributesDepositData{ + CanonicalTip: [32]byte(hash), + Headers: headers, + } + + serialized, err := btcDepData.MarshalBinary() + if err != nil { + t.Errorf("Error: unable to marshal binary: %v", err) + } + + expected, _ := hexutil.Decode("0xc94f1cca112233445566778811223344556677881122334455667788112233445566778800000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000") + if !bytes.Equal(serialized, expected) { + t.Errorf("Error: expected serialized %x but got %x instead", expected, serialized) + } + + var btcDepParsed BtcAttributesDepositData + err = btcDepParsed.UnmarshalBinary(serialized) + if err != nil { + t.Errorf("Error: unable to unmarshal serialized data %x, err: %v", serialized[:], err) + } + + if !bytes.Equal(hash[:], btcDepParsed.CanonicalTip[:]) { + t.Errorf("Error: hash was not unserialized correctly, expected %x but got %x instead", hash[:], btcDepParsed.CanonicalTip[:]) + } +} + +func TestBtcAttributesDepositDataOneHeader(t *testing.T) { + hash, _ := hexutil.Decode("0x1122334455667788112233445566778811223344556677881122334455667788") + + headers := make([][80]byte, 1) + h0, _ := hexutil.Decode("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + headers[0] = [80]byte(h0) + btcDepData := BtcAttributesDepositData{ + CanonicalTip: [32]byte(hash), + Headers: headers, + } + + serialized, err := btcDepData.MarshalBinary() + if err != nil { + t.Errorf("Error: unable to marshal binary: %v", err) + } + + expected, _ := hexutil.Decode("0xc94f1cca11223344556677881122334455667788112233445566778811223344556677880000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000050aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa00000000000000000000000000000000") + if !bytes.Equal(serialized, expected) { + t.Errorf("Error: expected serialized %x but got %x instead", expected, serialized) + } + + var btcDepParsed BtcAttributesDepositData + err = btcDepParsed.UnmarshalBinary(serialized) + if err != nil { + t.Errorf("Error: unable to unmarshal serialized data %x, err: %v", serialized[:], err) + } + + if !bytes.Equal(hash[:], btcDepParsed.CanonicalTip[:]) { + t.Errorf("Error: hash was not unserialized correctly, expected %x but got %x instead", hash[:], btcDepParsed.CanonicalTip[:]) + } + + if !bytes.Equal(h0[:], btcDepParsed.Headers[0][:]) { + t.Errorf("Error: header was not unserialized correctly, expected %x but got %x instead", h0[:], btcDepParsed.Headers[0][:]) + } +} + +func TestBtcAttributesDepositDataSixHeaders(t *testing.T) { + hash, _ := hexutil.Decode("0x1122334455667788112233445566778811223344556677881122334455667788") + + headers := make([][80]byte, 6) + h0, _ := hexutil.Decode("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + h1, _ := hexutil.Decode("0xbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + h2, _ := hexutil.Decode("0xcaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + h3, _ := hexutil.Decode("0xdaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + h4, _ := hexutil.Decode("0xeaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + h5, _ := hexutil.Decode("0xfaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") + headers[0] = [80]byte(h0) + headers[1] = [80]byte(h1) + headers[2] = [80]byte(h2) + headers[3] = [80]byte(h3) + headers[4] = [80]byte(h4) + headers[5] = [80]byte(h5) + btcDepData := BtcAttributesDepositData{ + CanonicalTip: [32]byte(hash), + Headers: headers, + } + + serialized, err := btcDepData.MarshalBinary() + if err != nil { + t.Errorf("Error: unable to marshal binary: %v", err) + } + + expected, _ := hexutil.Decode("0xc94f1cca11223344556677881122334455667788112233445566778811223344556677880000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000001c0000000000000000000000000000000000000000000000000000000000000024000000000000000000000000000000000000000000000000000000000000002c000000000000000000000000000000000000000000000000000000000000003400000000000000000000000000000000000000000000000000000000000000050aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000050baaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000050caaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000050daaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000050eaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000050faaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa00000000000000000000000000000000") + if !bytes.Equal(serialized, expected) { + t.Errorf("Error: expected serialized %x but got %x instead", expected, serialized) + } + + var btcDepParsed BtcAttributesDepositData + err = btcDepParsed.UnmarshalBinary(serialized) + if err != nil { + t.Errorf("Error: unable to unmarshal serialized data %x, err: %v", serialized[:], err) + } + + if !bytes.Equal(hash[:], btcDepParsed.CanonicalTip[:]) { + t.Errorf("Error: hash was not unserialized correctly, expected %x but got %x instead", hash[:], btcDepParsed.CanonicalTip[:]) + } + + if !bytes.Equal(h0[:], btcDepParsed.Headers[0][:]) { + t.Errorf("Error: hgeader was not unserialized correctly, expected %x but got %x instead", h0[:], btcDepParsed.Headers[0][:]) + } + if !bytes.Equal(h1[:], btcDepParsed.Headers[1][:]) { + t.Errorf("Error: hgeader was not unserialized correctly, expected %x but got %x instead", h1[:], btcDepParsed.Headers[1][:]) + } + if !bytes.Equal(h2[:], btcDepParsed.Headers[2][:]) { + t.Errorf("Error: hgeader was not unserialized correctly, expected %x but got %x instead", h2[:], btcDepParsed.Headers[2][:]) + } + if !bytes.Equal(h3[:], btcDepParsed.Headers[3][:]) { + t.Errorf("Error: hgeader was not unserialized correctly, expected %x but got %x instead", h3[:], btcDepParsed.Headers[3][:]) + } + if !bytes.Equal(h4[:], btcDepParsed.Headers[4][:]) { + t.Errorf("Error: hgeader was not unserialized correctly, expected %x but got %x instead", h4[:], btcDepParsed.Headers[4][:]) + } + if !bytes.Equal(h5[:], btcDepParsed.Headers[5][:]) { + t.Errorf("Error: hgeader was not unserialized correctly, expected %x but got %x instead", h5[:], btcDepParsed.Headers[5][:]) + } +} diff --git a/core/types/btc_attributes_deposited_tx.go b/core/types/btc_attributes_deposited_tx.go new file mode 100644 index 0000000000..a5bf5c06a9 --- /dev/null +++ b/core/types/btc_attributes_deposited_tx.go @@ -0,0 +1,91 @@ +// Copyright 2021 The go-ethereum Authors +// Copyright 2023 Bloq, Inc. +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package types + +import ( + "bytes" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/rlp" +) + +const ( + BtcAttributesDepositedTxType = 0x7C + + BtcAttributesDepositedSender = "0x8888888888888888888888888888888888888888" +) + +var ( + BtcAttributesDepositedSenderAddress = common.HexToAddress(BtcAttributesDepositedSender) +) + +type BtcAttributesDepositedTx struct { + // Will always be HvmState contract address + To *common.Address `rlp:"nil"` + + // gas limit + Gas uint64 + + // ABI-encoded smart contract call to HvmState.Update() + Data []byte +} + +// copy creates a deep copy of the transaction data and initializes all fields. +func (tx *BtcAttributesDepositedTx) copy() TxData { + cpy := &BtcAttributesDepositedTx{ + To: copyAddressPtr(tx.To), + Gas: tx.Gas, + Data: common.CopyBytes(tx.Data), + } + + return cpy +} + +func (tx *BtcAttributesDepositedTx) txType() byte { return BtcAttributesDepositedTxType } +func (tx *BtcAttributesDepositedTx) chainID() *big.Int { return common.Big0 } // Compatibility - Unused +func (tx *BtcAttributesDepositedTx) accessList() AccessList { return nil } // Compatibility - Unused +func (tx *BtcAttributesDepositedTx) data() []byte { return tx.Data } +func (tx *BtcAttributesDepositedTx) gas() uint64 { return tx.Gas } +func (tx *BtcAttributesDepositedTx) gasFeeCap() *big.Int { return new(big.Int) } +func (tx *BtcAttributesDepositedTx) gasTipCap() *big.Int { return new(big.Int) } +func (tx *BtcAttributesDepositedTx) gasPrice() *big.Int { return new(big.Int) } +func (tx *BtcAttributesDepositedTx) value() *big.Int { return new(big.Int) } // Compatibility - Unused +func (tx *BtcAttributesDepositedTx) nonce() uint64 { return 0 } // Compatibility - actual nonce set during execution +func (tx *BtcAttributesDepositedTx) to() *common.Address { return tx.To } +func (tx *BtcAttributesDepositedTx) isSystemTx() bool { return false } // Compatibility - Unused + +func (tx *BtcAttributesDepositedTx) effectiveGasPrice(dst *big.Int, baseFee *big.Int) *big.Int { + return dst.Set(new(big.Int)) +} + +func (tx *BtcAttributesDepositedTx) rawSignatureValues() (v, r, s *big.Int) { + return common.Big0, common.Big0, common.Big0 +} + +func (tx *BtcAttributesDepositedTx) setSignatureValues(chainID, v, r, s *big.Int) { + // this is a noop for pop transactions +} + +func (tx *BtcAttributesDepositedTx) encode(b *bytes.Buffer) error { + return rlp.Encode(b, tx) +} + +func (tx *BtcAttributesDepositedTx) decode(input []byte) error { + return rlp.DecodeBytes(input, tx) +} diff --git a/core/types/transaction.go b/core/types/transaction.go index babe2bb023..97e6626fd1 100644 --- a/core/types/transaction.go +++ b/core/types/transaction.go @@ -215,6 +215,8 @@ func (tx *Transaction) decodeTyped(b []byte) (TxData, error) { inner = new(DepositTx) case PopPayoutTxType: inner = new(PopPayoutTx) + case BtcAttributesDepositedTxType: + inner = new(BtcAttributesDepositedTx) default: return nil, ErrTxTypeNotSupported } @@ -361,6 +363,11 @@ func (tx *Transaction) IsPopPayoutTx() bool { return tx.Type() == PopPayoutTxType } +// IsBtcAttributesDepositedTx returns true if the transaction is a Bitcoin Attributes Deposited tx type. +func (tx *Transaction) IsBtcAttributesDepositedTx() bool { + return tx.Type() == BtcAttributesDepositedTxType +} + // IsSystemTx returns true for deposits that are system transactions. These transactions // are executed in an unmetered environment & do not contribute to the block gas limit. func (tx *Transaction) IsSystemTx() bool { @@ -610,6 +617,33 @@ type Transactions []*Transaction // Len returns the length of s. func (s Transactions) Len() int { return len(s) } +func (s Transactions) ExtractBtcAttrData() (*BtcAttributesDepositData, error) { + var extracted *BtcAttributesDepositData + for _, tx := range s { + // BtcAttributesDeposited tx expected to always be at index 1 (no PoP tx present) + // or 2 (PoP tx present) if it exists at all, but allow it anywhere here to be + //compatible even if transaction ordering changes. + if tx.IsBtcAttributesDepositedTx() { + if extracted != nil { + return nil, fmt.Errorf("transactions contain more than one Bitcoin Attributes Deposited transaction") + } + + data := tx.Data() + + var btcDepParsed BtcAttributesDepositData + err := btcDepParsed.UnmarshalBinary(data) + + if err != nil { + return nil, err + } + + extracted = &btcDepParsed + } + } + + return extracted, nil +} + // EncodeIndex encodes the i'th transaction to w. Note that this does not check for errors // because we assume that *Transaction will only ever contain valid txs that were either // constructed by decoding or via public API in this package. diff --git a/core/vm/contracts.go b/core/vm/contracts.go index ee35e6046d..635a83afa9 100644 --- a/core/vm/contracts.go +++ b/core/vm/contracts.go @@ -23,6 +23,10 @@ import ( "encoding/binary" "errors" "fmt" + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg" + "github.com/btcsuite/btcd/wire" + "github.com/hemilabs/heminetwork/database" "math/big" "reflect" @@ -37,7 +41,6 @@ import ( "github.com/ethereum/go-ethereum/crypto/kzg4844" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/params" - "github.com/hemilabs/heminetwork/database/tbcd" "github.com/hemilabs/heminetwork/service/tbc" "golang.org/x/crypto/ripemd160" "golang.org/x/exp/slices" @@ -54,189 +57,547 @@ type PrecompiledContract interface { type hVMQueryKey [32]byte -var TBCIndexer *tbc.Server -var initReady bool +var TBCFullNode *tbc.Server +var tbcChainParams *chaincfg.Params // TODO: Cache this on-disk at some point, will need to persist restarts to correctly provide execution traces for old txs var hvmQueryMap = make(map[hVMQueryKey][]byte) var HvmNullBlockHash = make([]byte, 32) -// SetInitReady TODO: Review, refactor initialization to its own method that accepts initial chain BTC block configuration -func SetInitReady() { - initReady = true +func GetTBCFullNodeSyncStatus() *tbc.SyncInfo { + syncInfo := TBCFullNode.Synced(context.Background()) + return &syncInfo } -// TODO: Remove this logic once TBC progression is driven by BTC Attributes Deposited tx -const TBCTipHeightLag = 2 -const TBCTipTimestampLag = 60 * 10 // 10 minutes -const TBCMaxBlocksPerProgression = 6 +// SetupTBCFullNode Sets up the TBC full node that will be available for precompiles +func SetupTBCFullNode(ctx context.Context, cfg *tbc.Config) error { + switch cfg.Network { + case "mainnet": + tbcChainParams = &chaincfg.MainNetParams + case "testnet3": + tbcChainParams = &chaincfg.TestNet3Params + case "localnet": + tbcChainParams = &chaincfg.RegressionNetParams + default: + log.Crit("TBC configured with an unknown network!", "network", cfg.Network) + } -// ProgressTip For now, this update after a new head is set processes TBC updates before construction -// on a new block payload occurs, so that tx execution will always be the same for the new -// block being built. Halts if any TBC issues occur for debugging. -func ProgressTip(ctx context.Context, currentTimestamp uint32) { - log.Info("Progressing TBC tip...", "currentTimestamp", currentTimestamp) - if TBCIndexer == nil { - log.Warn("TBCIndexer is nil, no-op") - return - } else { - log.Info("TBCIndexer is not nil") + tbcNode, err := tbc.NewServer(cfg) + if err != nil { + log.Crit("Unable to create TBC node!", "err", err) + return err } - si := TBCIndexer.Synced(context.Background()) - uh := si.Utxo.Height - th := si.Tx.Height - log.Info("Checking for TBC progression available", "utxoIndexHeight", uh, "txIndexHeight", th) + go func() { + err := tbcNode.Run(ctx) + if err != nil && err != context.Canceled { + panic(err) + } + }() + + TBCFullNode = tbcNode - if uh != th { - log.Crit("TBC is in an unexpected state, utxoIndexHeight != txIndexHeight!", - "utxoIndexHeight", uh, "txIndexHeight", th) - } + return nil +} - tipHeight, _, err := TBCIndexer.BlockHeaderBest(ctx) +// Very expensive but only needed for recovery. +// If needed for other things in the future, update TBC to efficiently handle this query. +func getCanonicalHeaderAtHeight(height uint64) *wire.BlockHeader { + height, header, err := TBCFullNode.BlockHeaderBest(context.Background()) if err != nil { - log.Crit("Unable to retrieve tip headers from TBC", "err", err) + // TODO: Recovery? + log.Crit("Unable to get best block header from TBC Full Node!") } - log.Info(fmt.Sprintf("TBC download status: tipHeight=%d", tipHeight)) - // Tip height is more than 2 blocks ahead, TBC can progress as far as timestamp is sufficiently past - if tipHeight > uh+TBCTipHeightLag { - endingHeight := uh - for height := uh + 1; height <= tipHeight-TBCTipHeightLag && height <= uh+TBCMaxBlocksPerProgression; height++ { - headersAtHeight, err := TBCIndexer.BlockHeadersByHeight(ctx, height) - if err != nil { - log.Crit("Unable to retrieve headers from TBC", "height", height) - } - for _, h := range headersAtHeight { - if uint32(h.Timestamp.Unix())+TBCTipTimestampLag > currentTimestamp { - break - } - } - endingHeight = height + cursor := header + cursorHeight := height + // Walk back best tip until we get to the destination height + for cursorHeight > height { + prevHeader, prevHeight, err := TBCFullNode.BlockHeaderByHash(context.Background(), &cursor.PrevBlock) + if err != nil { + // TODO: Recovery? + log.Crit("Unable to get block %x from TBC Full Node!", cursor.PrevBlock[:]) } - if endingHeight > uh { - startingHeight := uh + 1 - // TBC has block headers to progress, check that the actual blocks are all available - hasAllBlocks := TBCBlocksAvailableToHeight(ctx, startingHeight, endingHeight) - if !hasAllBlocks { - log.Info("TBC does not have all blocks, will progress chain later", "start", - startingHeight, "end", endingHeight) - return - } + cursor = prevHeader + cursorHeight = prevHeight // pedantic + } - headers, err := TBCIndexer.BlockHeadersByHeight(ctx, endingHeight) - if err != nil { - log.Crit(fmt.Sprintf("could not get BlockHeadersByHeight %v", err)) - } + return cursor +} - if len(headers) != 1 { - log.Crit(fmt.Sprintf("received unexpected headers length %d", len(headers))) - } +// TODO: Refactor this, IsEqual was creating issues +func hashEquals(a chainhash.Hash, b chainhash.Hash) bool { + return bytes.Equal(a[:], b[:]) +} - hash := headers[0].BlockHash() +// Walks backwards from both headers to find a common ancestor. +// Returns common ancestor header, a boolean for whether there was a fork or one of the passed +// in headers was an ancestor of the other +// TODO: Refactor this, also could return height to make some upstream uses easier +func findCommonAncestor(a *tbc.HashHeight, b *tbc.HashHeight) (*wire.BlockHeader, bool, error) { + if a.Hash.IsEqual(&b.Hash) { + header, _, err := TBCFullNode.BlockHeaderByHash(context.Background(), &a.Hash) + if err != nil { + return nil, false, err + } + return header, false, nil // They are same, no fork + } - TBCIndexer.SyncIndexersToHash(ctx, &hash) + lowerHeight := a.Height + higherHash := b.Hash + lowerHash := a.Hash + if b.Height < lowerHeight { + lowerHeight = b.Height + higherHash = a.Hash + lowerHash = b.Hash + } - done := TBCIndexer.Synced(ctx) - if done.Utxo.Height != endingHeight { - log.Crit(fmt.Sprintf("After indexing to block %d, UtxoHeight=%d!", endingHeight, done.Utxo.Height)) - } - if done.Tx.Height != endingHeight { - log.Crit(fmt.Sprintf("After indexing to block %d, TxHeight=%d!", endingHeight, done.Tx.Height)) - } - log.Info("TBC progression done!", "utxoHeight", done.Utxo.Height, "txHeight", done.Tx.Height) + highCursorHeader, highCursorHeight, err := TBCFullNode.BlockHeaderByHash(context.Background(), &higherHash) + if err != nil { + return nil, false, err + } + + lowCursorHeader, lowCursorHeight, err := TBCFullNode.BlockHeaderByHash(context.Background(), &lowerHash) + if err != nil { + return nil, false, err + } + + // TODO: Redundant heights + for highCursorHeight > lowCursorHeight { + highCursorHeader, highCursorHeight, err = TBCFullNode.BlockHeaderByHash(context.Background(), &highCursorHeader.PrevBlock) + if err != nil { + return nil, false, err } } + + // If the cursors are now equal then one was the ancestor + if hashEquals(lowCursorHeader.BlockHash(), highCursorHeader.BlockHash()) { + return lowCursorHeader, false, nil // No fork + } + + // Cursors are at the same height but on different forks, walk both of them back until they match + // TODO can just do this for loop and ignore the if condition above + for !hashEquals(lowCursorHeader.BlockHash(), highCursorHeader.BlockHash()) { + lowCursorHeader, lowCursorHeight, err = TBCFullNode.BlockHeaderByHash(context.Background(), &lowCursorHeader.PrevBlock) + if err != nil { + return nil, false, err + } + highCursorHeader, highCursorHeight, err = TBCFullNode.BlockHeaderByHash(context.Background(), &highCursorHeader.PrevBlock) + if err != nil { + return nil, false, err + } + } + + // Now the cursors match + return lowCursorHeader, true, nil // There was a fork } -func TBCBlocksAvailableToHeight(ctx context.Context, startingHeight uint64, endingHeight uint64) bool { - // See if endingHeight header exists - blockHeaders, err := TBCIndexer.DB().BlockHeadersByHeight(ctx, endingHeight) +// Moves the Tx indexer to the specified header. This does not +// assume that the move to header is straight - it will determine +// whether a fork is required and handle it appropriately. +// This should only be used when recovering from a desync between +// the indexers, otherwise always use moveIndexersToHeight +// TODO: Dedup with moveTxIndexerToUtxo & TBCIndexToHeader logic somehow? +func moveTxIndexerToHeader(header *wire.BlockHeader) error { + tIndexInfo, err := TBCFullNode.TxIndexHash(context.Background()) + headerHash := header.BlockHash() if err != nil { - log.Error("Unable to get block headers from TBC at ending height", "endingHeight", endingHeight) - return false + // Critical error + log.Crit(fmt.Sprintf("Unable to move TBC full node Tx indexer to block %x; unable to get TxIndexHash", + headerHash[:]), "err", err) } - if len(blockHeaders) == 0 { - // endingHeight header does not exist - log.Info("TBC does not have header for ending block", "endingHeight", endingHeight) - return false + if hashEquals(tIndexInfo.Hash, header.BlockHash()) { + // already done + return nil } - bestHash := blockHeaders[0].Hash - - // See if endingHeight block exists - block, err := TBCIndexer.DB().BlockByHash(ctx, bestHash) + targetHash := header.BlockHash() + _, targetHeight, err := TBCFullNode.BlockHeaderByHash(context.Background(), &targetHash) if err != nil { - log.Info("TBC not synced, no block downloaded for final header", "tip", blockHeaders[0].Height) - return false + // Passed in header is not available + return err } - if block == nil { - log.Info("Block at endingHeight is nil", "endingHeight", endingHeight) - return false + targetHH := &tbc.HashHeight{ + Height: targetHeight, + Hash: targetHash, } - // Tip has been downloaded, so do expensive check of all blocks up to tip - log.Info("TBC has endingHeight block synced, checking blocks in-between...") - for htc := startingHeight; htc < endingHeight; htc++ { - if htc%10000 == 0 { - log.Debug(fmt.Sprintf("Checking for contiguous blocks between %d and %d,"+ - " current block check for %d...", - startingHeight, endingHeight, htc)) + ancestor, isFork, err := findCommonAncestor(tIndexInfo, targetHH) + if err != nil { + // Critical error + log.Crit(fmt.Sprintf("Unable to find common ancestor between tx indexer tip %x and best header %x", + tIndexInfo.Hash[:], targetHH.Hash[:])) + } + ancestorHash := ancestor.BlockHash() + + if !isFork { + // Tx indexer only needs to move in one direction, and TxIndexer will figure out which + err = TBCFullNode.TxIndexer(context.Background(), &targetHH.Hash) + if err != nil { + log.Error("Unable to move Tx indexer from current hash %x to requested hash %x", + tIndexInfo.Hash[:], targetHH.Hash[:]) + return err + } + } else { + // Tx indexer needs to first unwind to the ancestor, and then wind to the requested target + err = TBCFullNode.TxIndexer(context.Background(), &ancestorHash) + if err != nil { + log.Error("While indexing over a fork, unable to unwind Tx indexer from current hash "+ + "%x to requested hash %x", tIndexInfo.Hash[:], ancestorHash[:]) + return err + } + // We unwound to common ancestor, now need to wind forward + err = TBCFullNode.TxIndexer(context.Background(), &targetHH.Hash) + if err != nil { + log.Error("While indexing over a fork, unable to wind Tx indexer from current hash "+ + "%x to requested hash %x", ancestorHash[:], targetHH.Hash[:]) } + } + + // Successful + return nil +} + +// Moves the UTXO indexer to the specified header. This does not +// assume that the move to header is straight - it will determine +// whether a fork is required and handle it appropriately. +// This should only be used when recovering from a desync between +// the indexers, otherwise always use moveIndexersToHeight +// TODO: Dedup with moveTxIndexerToHeader / TBCIndexToHeader logic somehow? +func moveUtxoIndexerToHeader(header *wire.BlockHeader) error { + uIndexInfo, err := TBCFullNode.UtxoIndexHash(context.Background()) + headerHash := header.BlockHash() + if err != nil { + // Critical error + log.Crit(fmt.Sprintf("Unable to move TBC full node UTXO indexer to block %x; unable to get UtxoIndexHash", + headerHash[:]), "err", err) + } + + if hashEquals(uIndexInfo.Hash, header.BlockHash()) { + // already done + return nil + } + + targetHash := header.BlockHash() + _, targetHeight, err := TBCFullNode.BlockHeaderByHash(context.Background(), &targetHash) + if err != nil { + // Passed in header is not available + return err + } - blockHeadersCheck, err := TBCIndexer.DB().BlockHeadersByHeight(ctx, htc) + targetHH := &tbc.HashHeight{ + Height: targetHeight, + Hash: targetHash, + } + + ancestor, isFork, err := findCommonAncestor(uIndexInfo, targetHH) + if err != nil { + // Critical error + log.Crit(fmt.Sprintf("Unable to find common ancestor between utxo indexer tip %x and best header %x", + uIndexInfo.Hash[:], targetHH.Hash[:])) + } + ancestorHash := ancestor.BlockHash() + + if !isFork { + // UTXO indexer only needs to move in one direction, and UtxoIndexer will figure out which + err = TBCFullNode.UtxoIndexer(context.Background(), &targetHH.Hash) + if err != nil { + log.Error("Unable to move UTXO indexer from current hash %x to requested hash %x", + uIndexInfo.Hash[:], targetHH.Hash[:]) + return err + } + } else { + // UTXO indexer needs to first unwind to the ancestor, and then wind to the requested target + err = TBCFullNode.UtxoIndexer(context.Background(), &ancestorHash) + if err != nil { + log.Error("While indexing over a fork, unable to unwind UTXO indexer from current hash "+ + "%x to requested hash %x", uIndexInfo.Hash[:], ancestorHash[:]) + return err + } + // We unwound to common ancestor, now need to wind forward + err = TBCFullNode.UtxoIndexer(context.Background(), &targetHH.Hash) if err != nil { - log.Error("Unable to get best block headers from TBC at intermediate height", "heightToCheck", htc) - return false + log.Error("While indexing over a fork, unable to wind UTXO indexer from current hash "+ + "%x to requested hash %x", ancestorHash[:], targetHH.Hash[:]) } + } + + // Successful + return nil +} - if len(blockHeadersCheck) == 0 { - log.Info("TBC does not have header for intermediate block", "heightToCheck", htc) +// fixMismatchedIndexesIfRequired checks if the UTXO and TX indexers do not match, +// and if they don't walks both of them back to the highest common ancestor. +// This should only ever be required if something like an unclean +// shutdown resulted in TBC's indexers being off. +func fixMismatchedIndexesIfRequired() error { + uIndexInfo, err := TBCFullNode.UtxoIndexHash(context.Background()) + if err != nil { + log.Crit("Unable to get UtxoIndexHash", "err", err) + } + tIndexInfo, err := TBCFullNode.TxIndexHash(context.Background()) + if err != nil { + log.Crit("Unable to get TxIndexHash", "err", err) + } + + if !hashEquals(uIndexInfo.Hash, tIndexInfo.Hash) { + // Find the common ancestor + ancestor, _, err := findCommonAncestor(uIndexInfo, tIndexInfo) + if err != nil { + log.Error(fmt.Sprintf("Unable to find common ancestor between Utxo and Tx indexers! "+ + "Utxo indexed to: %x, Tx indexed to: %x", uIndexInfo.Hash[:], tIndexInfo.Hash[:])) } - // TODO: Check that block is part of canonical chain - intermediateHash := blockHeadersCheck[0].Hash + ancestorHash := ancestor.BlockHash() + log.Info(fmt.Sprintf("Fixing mismatched UTXO and Tx indexes, UTXO indexer @ %x, "+ + "Tx indexer @ %x, common ancestor @ %x", uIndexInfo.Hash[:], tIndexInfo.Hash[:], ancestorHash[:])) + + ancestorHeader, _, err := TBCFullNode.BlockHeaderByHash(context.Background(), &ancestorHash) - intermediateBlock, err := TBCIndexer.DB().BlockByHash(ctx, intermediateHash) - if err != nil || intermediateBlock == nil { - log.Info("TBC not synced, no block downloaded for intermediate header", "intermediateHash", intermediateHash) - return false + // Rewind both to common ancestor + err = moveUtxoIndexerToHeader(ancestorHeader) + if err != nil { + log.Crit(fmt.Sprintf("Unable to repair indexer desync by moving UTXO indexer "+ + "from %x to common ancestor %x", uIndexInfo.Hash[:], ancestorHash[:])) + } + err = moveTxIndexerToHeader(ancestorHeader) + if err != nil { + log.Crit(fmt.Sprintf("Unable to repair indexer desync by moving Tx indexer "+ + "from %x to common ancestor %x", tIndexInfo.Hash[:], ancestorHash[:])) } - // If execution got here, then the block and its header are downloaded, continue looking - log.Trace("Block found, continuing contiguous blocks search", "heightToCheck", htc) } - log.Info("TBC synced, has all blocks downloaded from starting height to ending height", "startingHeight", startingHeight, "endingHeight", endingHeight, "tipHash", bestHash) - return true + + return nil } -func SetupTBC(ctx context.Context, cfg *tbc.Config) error { - tbcNode, err := tbc.NewServer(cfg) +// TBCIndexToHeader is a convenience pass-through to TBCIndexToHashHeight with +// a Bitcoin header provided. +func TBCIndexToHeader(header *wire.BlockHeader) error { + targetHash := header.BlockHash() + _, targetHeight, err := TBCFullNode.BlockHeaderByHash(context.Background(), &targetHash) if err != nil { - log.Crit("Unable to create TBC node!", "err", err) + // Passed in header is not available return err } - go func() { - err := tbcNode.Run(ctx) - if err != nil && err != context.Canceled { - panic(err) + hh := tbc.HashHeight{ + Hash: header.BlockHash(), + Height: targetHeight, + } + + return TBCIndexToHashHeight(&hh) +} + +// TBCRestoreIndexersToPoint attempts to move the TBC Full Node's UTXO +// and Tx indexers back to their respective points from a prior SyncInfo. +// Under normal operation the UTXO and Tx index tips should always be +// the same, but this method will restore UTXO and Tx indexers to different +// states if specified by the passed-in Syncinfo. +func TBCRestoreIndexersToPoint(point *tbc.SyncInfo) error { + utxoPoint := point.Utxo + utxoHeader, _, err := TBCFullNode.BlockHeaderByHash(context.Background(), &utxoPoint.Hash) + if err != nil { + return err + } + err = moveUtxoIndexerToHeader(utxoHeader) + if err != nil { + return err + } + + txPoint := point.Tx + txHeader, _, err := TBCFullNode.BlockHeaderByHash(context.Background(), &txPoint.Hash) + if err != nil { + return err + } + err = moveTxIndexerToHeader(txHeader) + if err != nil { + return err + } + return nil +} + +// TBCIndexToHashHeight first checks to make sure the UTXO and Tx indexers +// are the same (and if not, moves both to the lowest indexed height of either) +// and then moves the indexer to the specified target hash and height, +// unwinding and winding if the move from current indexer state to new +// target state involves a reorganization. +func TBCIndexToHashHeight(targetHH *tbc.HashHeight) error { + // Check for indexer desync and attempt to fix. + err := fixMismatchedIndexesIfRequired() + if err != nil { + log.Crit(fmt.Sprintf("Unable to fix mismatched indexes")) + } + + targetHash := targetHH.Hash + + // Already checked for indexer desync so if we got here UTXO and Tx indexes are the same + tIndexInfo, err := TBCFullNode.TxIndexHash(context.Background()) + if err != nil { + // Critical error + log.Crit("Unable to move TBC full node indexers to block %x; unable to get TxIndexHash", "err", err) + } + + if hashEquals(tIndexInfo.Hash, targetHash) { + // already done + return nil + } + + ancestor, isFork, err := findCommonAncestor(tIndexInfo, targetHH) + if err != nil { + // Critical error + log.Crit(fmt.Sprintf("Unable to find common ancestor between indexers tip %x and best header %x", + tIndexInfo.Hash[:], targetHH.Hash[:])) + } + ancestorHash := ancestor.BlockHash() + + if !isFork { + // Indexers only needs to move in one direction, and the indexer will figure out which + err = TBCFullNode.SyncIndexersToHash(context.Background(), &targetHH.Hash) + if err != nil { + log.Error("Unable to move indexers from current hash %x to requested hash %x", + tIndexInfo.Hash[:], targetHH.Hash[:]) + return err } - }() + } else { + // Indexers need to first unwind to the ancestor, and then wind to the requested target + err = TBCFullNode.SyncIndexersToHash(context.Background(), &ancestorHash) + if err != nil { + log.Error("While indexing over a fork, unable to unwind indexers from current hash "+ + "%x to requested hash %x", tIndexInfo.Hash[:], ancestorHash[:]) + return err + } + // We unwound to common ancestor, now need to wind forward + err = TBCFullNode.SyncIndexersToHash(context.Background(), &targetHH.Hash) + if err != nil { + log.Error("While indexing over a fork, unable to wind indexers from current hash "+ + "%x to requested hash %x", ancestorHash[:], targetHH.Hash[:]) + } + } - TBCIndexer = tbcNode + // Successful return nil } -var hvmContractsToAddress = map[reflect.Type][]byte{ - reflect.TypeOf(&btcBalAddr{}): {0x40}, - reflect.TypeOf(&btcUtxosAddrList{}): {0x41}, - reflect.TypeOf(&btcTxByTxid{}): {0x42}, - reflect.TypeOf(&btcTxConfirmations{}): {0x43}, - reflect.TypeOf(&btcLastHeader{}): {0x44}, - reflect.TypeOf(&btcHeaderN{}): {0x45}, +func hashHeightForHeader(ctx context.Context, header *wire.BlockHeader) (*tbc.HashHeight, error) { + hash := header.BlockHash() + _, height, err := TBCFullNode.BlockHeaderByHash(ctx, &hash) + if err != nil { + return nil, err + } + + return &tbc.HashHeight{Hash: hash, Height: height}, nil +} + +// TBCBlocksAvailableToHeader Checks whether the TBC full node has all of the blocks required to index to the +// specified header from its current location. +// +// This function assumes that any blocks below the current indexed tip are available, otherwise the indexers +// would have been unable to reach that tip previously. +// +// This function will always return true if the specified header is a direct ancestor of current indexed tip, +// including if they are equal. +// +// If this function is called with a header that requires a reorg, it finds the common ancestor and returns +// whether all blocks required to index after walking back to that common ancestor are available. +// +// If TBC's UTXO and Tx indexers are not in the same state, this function will determine whether all blocks +// are available based on the commnon ancestor of the misaligned indexer tips (such that reconciling the +// indexer tips and then moving to the specified endingHeader would have all required blocks. +func TBCBlocksAvailableToHeader(ctx context.Context, endingHeader *wire.BlockHeader) (bool, error) { + syncInfo := TBCFullNode.Synced(ctx) + utxoSync := syncInfo.Utxo + txSync := syncInfo.Tx + + // When both indexers are at the same header, this will be that header. + // If the indexers are at different positions, this will be the common + // ancestor they share, which we know we could walk back to since the + // blocks were available to index to the two different tips + commonIndexTip, _, err := findCommonAncestor(&utxoSync, &txSync) + if err != nil { + if errors.As(err, &database.ErrNotFound) { + return false, nil + } + return false, err + } + tipHH, err := hashHeightForHeader(ctx, commonIndexTip) + if err != nil { + if errors.As(err, &database.ErrNotFound) { + return false, nil + } + return false, err + } + targetHH, err := hashHeightForHeader(ctx, endingHeader) + if err != nil { + if errors.As(err, &database.ErrNotFound) { + return false, nil + } + return false, err + } + + // Find common ancestor between current common index ancestor tip and target header + ancestorToTarget, _, err := findCommonAncestor(tipHH, targetHH) + if err != nil { + if errors.As(err, &database.ErrNotFound) { + return false, nil + } + return false, err + } + ancestorToTargetHash := ancestorToTarget.BlockHash() + _, ancestorHeight, err := TBCFullNode.BlockHeaderByHash(ctx, &ancestorToTargetHash) + if err != nil { + if errors.As(err, &database.ErrNotFound) { + return false, nil + } + return false, err + } + + // Whether or not moving to the target requires unwinding, the only blocks that + // could be missing are the ones that would have to be indexed after the rewind, + // so we only need to check for all blocks from the ancestor to the target. + // Walk backwards from the target down to the ancestor, as generally if blocks + // are missing they will be towards the end so top down will find more efficiently. + // TODO: make more efficient by adding a cheap check in TBC for a full block being available. + endingHash := endingHeader.BlockHash() + cursor, height, err := TBCFullNode.BlockHeaderByHash(ctx, &endingHash) + if err != nil { + return false, err + } + cursorHash := endingHash + + // Walk backwards until our cursor matches the ancestor + for !bytes.Equal(cursorHash[:], ancestorToTargetHash[:]) { + available, err := TBCFullNode.FullBlockAvailable(ctx, &cursorHash) + if err != nil { + return false, err + } + if !available { + return false, nil + } + + cursor, height, err = TBCFullNode.BlockHeaderByHash(ctx, &cursor.PrevBlock) + if err != nil { + if errors.As(err, &database.ErrNotFound) { + return false, nil + } + return false, err + } + if height < ancestorHeight { + // Somehow walking backwards got to a lower block than the ancestor we are looking for. + // Should never happen, would imply that the current indexed tip and target are not + // on the same chain graph + return false, fmt.Errorf("TBCBlocksAvailableToHeader failed walking backwards from"+ + " %x looking for %x, walked to height=%d but ancestorHeight=%d", endingHash[:], + ancestorToTargetHash[:], height, ancestorHeight) + } + cursorHash = cursor.PrevBlock + } + + // Above for loop exited meaning all blocks from the target back to common ancestor with + // indexer were successfully fetched from database + return true, nil } // PrecompiledContractsHomestead contains the default set of pre-compiled Ethereum @@ -278,62 +639,64 @@ var PrecompiledContractsIstanbul = map[common.Address]PrecompiledContract{ // PrecompiledContractsBerlin contains the default set of pre-compiled Ethereum // contracts used in the Berlin release. var PrecompiledContractsBerlin = map[common.Address]PrecompiledContract{ - common.BytesToAddress([]byte{1}): &ecrecover{}, - common.BytesToAddress([]byte{2}): &sha256hash{}, - common.BytesToAddress([]byte{3}): &ripemd160hash{}, - common.BytesToAddress([]byte{4}): &dataCopy{}, - common.BytesToAddress([]byte{5}): &bigModExp{eip2565: true}, - common.BytesToAddress([]byte{6}): &bn256AddIstanbul{}, - common.BytesToAddress([]byte{7}): &bn256ScalarMulIstanbul{}, - common.BytesToAddress([]byte{8}): &bn256PairingIstanbul{}, - common.BytesToAddress([]byte{9}): &blake2F{}, - common.BytesToAddress(hvmContractsToAddress[reflect.TypeOf(&btcBalAddr{})]): &btcBalAddr{}, - common.BytesToAddress(hvmContractsToAddress[reflect.TypeOf(&btcUtxosAddrList{})]): &btcUtxosAddrList{}, - common.BytesToAddress(hvmContractsToAddress[reflect.TypeOf(&btcTxByTxid{})]): &btcTxByTxid{}, - common.BytesToAddress(hvmContractsToAddress[reflect.TypeOf(&btcTxConfirmations{})]): &btcTxConfirmations{}, - common.BytesToAddress(hvmContractsToAddress[reflect.TypeOf(&btcLastHeader{})]): &btcLastHeader{}, - common.BytesToAddress(hvmContractsToAddress[reflect.TypeOf(&btcHeaderN{})]): &btcHeaderN{}, + common.BytesToAddress([]byte{1}): &ecrecover{}, + common.BytesToAddress([]byte{2}): &sha256hash{}, + common.BytesToAddress([]byte{3}): &ripemd160hash{}, + common.BytesToAddress([]byte{4}): &dataCopy{}, + common.BytesToAddress([]byte{5}): &bigModExp{eip2565: true}, + common.BytesToAddress([]byte{6}): &bn256AddIstanbul{}, + common.BytesToAddress([]byte{7}): &bn256ScalarMulIstanbul{}, + common.BytesToAddress([]byte{8}): &bn256PairingIstanbul{}, + common.BytesToAddress([]byte{9}): &blake2F{}, } // PrecompiledContractsCancun contains the default set of pre-compiled Ethereum // contracts used in the Cancun release. var PrecompiledContractsCancun = map[common.Address]PrecompiledContract{ - common.BytesToAddress([]byte{1}): &ecrecover{}, - common.BytesToAddress([]byte{2}): &sha256hash{}, - common.BytesToAddress([]byte{3}): &ripemd160hash{}, - common.BytesToAddress([]byte{4}): &dataCopy{}, - common.BytesToAddress([]byte{5}): &bigModExp{eip2565: true}, - common.BytesToAddress([]byte{6}): &bn256AddIstanbul{}, - common.BytesToAddress([]byte{7}): &bn256ScalarMulIstanbul{}, - common.BytesToAddress([]byte{8}): &bn256PairingIstanbul{}, - common.BytesToAddress([]byte{9}): &blake2F{}, - common.BytesToAddress([]byte{0x0a}): &kzgPointEvaluation{}, - common.BytesToAddress(hvmContractsToAddress[reflect.TypeOf(&btcBalAddr{})]): &btcBalAddr{}, - common.BytesToAddress(hvmContractsToAddress[reflect.TypeOf(&btcUtxosAddrList{})]): &btcUtxosAddrList{}, - common.BytesToAddress(hvmContractsToAddress[reflect.TypeOf(&btcTxByTxid{})]): &btcTxByTxid{}, - common.BytesToAddress(hvmContractsToAddress[reflect.TypeOf(&btcTxConfirmations{})]): &btcTxConfirmations{}, - common.BytesToAddress(hvmContractsToAddress[reflect.TypeOf(&btcLastHeader{})]): &btcLastHeader{}, - common.BytesToAddress(hvmContractsToAddress[reflect.TypeOf(&btcHeaderN{})]): &btcHeaderN{}, + common.BytesToAddress([]byte{1}): &ecrecover{}, + common.BytesToAddress([]byte{2}): &sha256hash{}, + common.BytesToAddress([]byte{3}): &ripemd160hash{}, + common.BytesToAddress([]byte{4}): &dataCopy{}, + common.BytesToAddress([]byte{5}): &bigModExp{eip2565: true}, + common.BytesToAddress([]byte{6}): &bn256AddIstanbul{}, + common.BytesToAddress([]byte{7}): &bn256ScalarMulIstanbul{}, + common.BytesToAddress([]byte{8}): &bn256PairingIstanbul{}, + common.BytesToAddress([]byte{9}): &blake2F{}, + common.BytesToAddress([]byte{0x0a}): &kzgPointEvaluation{}, } // PrecompiledContractsBLS contains the set of pre-compiled Ethereum // contracts specified in EIP-2537. These are exported for testing purposes. var PrecompiledContractsBLS = map[common.Address]PrecompiledContract{ - common.BytesToAddress([]byte{10}): &bls12381G1Add{}, - common.BytesToAddress([]byte{11}): &bls12381G1Mul{}, - common.BytesToAddress([]byte{12}): &bls12381G1MultiExp{}, - common.BytesToAddress([]byte{13}): &bls12381G2Add{}, - common.BytesToAddress([]byte{14}): &bls12381G2Mul{}, - common.BytesToAddress([]byte{15}): &bls12381G2MultiExp{}, - common.BytesToAddress([]byte{16}): &bls12381Pairing{}, - common.BytesToAddress([]byte{17}): &bls12381MapG1{}, - common.BytesToAddress([]byte{18}): &bls12381MapG2{}, + common.BytesToAddress([]byte{10}): &bls12381G1Add{}, + common.BytesToAddress([]byte{11}): &bls12381G1Mul{}, + common.BytesToAddress([]byte{12}): &bls12381G1MultiExp{}, + common.BytesToAddress([]byte{13}): &bls12381G2Add{}, + common.BytesToAddress([]byte{14}): &bls12381G2Mul{}, + common.BytesToAddress([]byte{15}): &bls12381G2MultiExp{}, + common.BytesToAddress([]byte{16}): &bls12381Pairing{}, + common.BytesToAddress([]byte{17}): &bls12381MapG1{}, + common.BytesToAddress([]byte{18}): &bls12381MapG2{}, +} + +var hvmContractsToAddress = map[reflect.Type][]byte{ + reflect.TypeOf(&btcBalAddr{}): {0x40}, + reflect.TypeOf(&btcUtxosAddrList{}): {0x41}, + reflect.TypeOf(&btcTxByTxid{}): {0x42}, + reflect.TypeOf(&btcTxConfirmations{}): {0x43}, + reflect.TypeOf(&btcLastHeader{}): {0x44}, + reflect.TypeOf(&btcHeaderN{}): {0x45}, + reflect.TypeOf(&btcAddrToScript{}): {0x46}, +} + +var PrecompiledContractsHvm0 = map[common.Address]PrecompiledContract{ common.BytesToAddress(hvmContractsToAddress[reflect.TypeOf(&btcBalAddr{})]): &btcBalAddr{}, common.BytesToAddress(hvmContractsToAddress[reflect.TypeOf(&btcUtxosAddrList{})]): &btcUtxosAddrList{}, common.BytesToAddress(hvmContractsToAddress[reflect.TypeOf(&btcTxByTxid{})]): &btcTxByTxid{}, common.BytesToAddress(hvmContractsToAddress[reflect.TypeOf(&btcTxConfirmations{})]): &btcTxConfirmations{}, common.BytesToAddress(hvmContractsToAddress[reflect.TypeOf(&btcLastHeader{})]): &btcLastHeader{}, common.BytesToAddress(hvmContractsToAddress[reflect.TypeOf(&btcHeaderN{})]): &btcHeaderN{}, + common.BytesToAddress(hvmContractsToAddress[reflect.TypeOf(&btcAddrToScript{})]): &btcAddrToScript{}, } var ( @@ -342,6 +705,7 @@ var ( PrecompiledAddressesIstanbul []common.Address PrecompiledAddressesByzantium []common.Address PrecompiledAddressesHomestead []common.Address + PrecompiledAddressesHvm0 []common.Address ) func init() { @@ -360,10 +724,13 @@ func init() { for k := range PrecompiledContractsCancun { PrecompiledAddressesCancun = append(PrecompiledAddressesCancun, k) } + for k := range PrecompiledContractsHvm0 { + // TODO: Does this assume bad indexes? Does this even need to be done? + PrecompiledAddressesHvm0 = append(PrecompiledAddressesHvm0, k) + } } -// ActivePrecompiles returns the precompiles enabled with the current configuration. -func ActivePrecompiles(rules params.Rules) []common.Address { +func activeUpstreamPrecompiles(rules params.Rules) []common.Address { switch { case rules.IsCancun: return PrecompiledAddressesCancun @@ -378,6 +745,24 @@ func ActivePrecompiles(rules params.Rules) []common.Address { } } +// ActivePrecompiles returns the precompiles enabled with the current configuration. +func ActivePrecompiles(rules params.Rules) []common.Address { + // For now, Hemi upgrades can be performed out-of-sync with upstream updates. + // As a result, this code is modified to select upstream precompiles, and then + // Layer on Hemi-specific precompile lists. + // Original ActivePrecompiles logic moved to activeUpstreamPrecompiles. + // TODO: Make this more efficient if necessary + + nonHvmPrecompiles := activeUpstreamPrecompiles(rules) + + switch { + case rules.IsHvm0: + return append(nonHvmPrecompiles, PrecompiledAddressesHvm0...) + default: + return nonHvmPrecompiles + } +} + // calculateHVMQueryKey constructs an hVMQueryKey which is used to cache hVM responses. // Each key is (precompile_input + precompile_address_byte + containing_header_hash) // This query key is unique for a specific precompile called with specific input argument contained in a specific block @@ -426,11 +811,14 @@ func (c *btcBalAddr) RequiredGas(input []byte) uint64 { } func (c *btcBalAddr) Run(input []byte, blockContext common.Hash) ([]byte, error) { - // TODO: 27 to global variable, check value - if input == nil || len(input) < 27 { + // TODO: 24 to global variable + if input == nil || len(input) < 24 { log.Debug("btcBalAddr run called with nil or too small input", "input", input) return nil, nil } + if TBCFullNode == nil { + log.Crit("hVM Precompile called but the TBC Full Node is not setup") + } var k hVMQueryKey if isValidBlock(blockContext) { @@ -440,7 +828,7 @@ func (c *btcBalAddr) Run(input []byte, blockContext common.Hash) ([]byte, error) } cachedResult, exists := hvmQueryMap[k] if exists { - log.Info(fmt.Sprintf("btcTxConfirmations returning cached result for query of "+ + log.Debug(fmt.Sprintf("btcTxConfirmations returning cached result for query of "+ "%x in context %x, cached result=%x", input, blockContext, cachedResult)) return cachedResult, nil } @@ -448,17 +836,16 @@ func (c *btcBalAddr) Run(input []byte, blockContext common.Hash) ([]byte, error) addr := string(input) log.Debug("btcBalAddr called", "address", addr) - if TBCIndexer == nil { + if TBCFullNode == nil { log.Crit("TBCIndexer is nil!") } - bal, err := TBCIndexer.BalanceByAddress(context.Background(), addr) - fmt.Printf("Balance: %d\n", bal) + bal, err := TBCFullNode.BalanceByAddress(context.Background(), addr) if err != nil { // TODO: Error handling - log.Debug("Unable to process balance of address! Cannot progress EVM.", "address", addr, "err", err) - bal = 0 // TODO: temp, change w/ error handling + log.Error("hVM Error: Unable to process balance of address", "address", addr, "err", err) + return nil, err } resp := make([]byte, 8) @@ -478,10 +865,10 @@ func (c *btcTxConfirmations) RequiredGas(input []byte) uint64 { func (c *btcTxConfirmations) Run(input []byte, blockContext common.Hash) ([]byte, error) { if input == nil || len(input) != 32 { - return nil, nil + log.Debug("btcTxConfirmations run called with nil or != 32 input", "input", fmt.Sprintf("%x", input)) } log.Debug("btcTxConfirmations called", "txid", input) - if TBCIndexer == nil { + if TBCFullNode == nil { log.Crit("TBCIndexer is nil!") } @@ -489,40 +876,51 @@ func (c *btcTxConfirmations) Run(input []byte, blockContext common.Hash) ([]byte if isValidBlock(blockContext) { k, err := calculateHVMQueryKey(input, hvmContractsToAddress[reflect.TypeOf(c)][0], blockContext) if err != nil { - log.Crit("Unable to calculate hVM Query Key!", "input", input, "blockContext", blockContext) + log.Error("Unable to calculate hVM Query Key!", + "input", fmt.Sprintf("%x", input), + "blockContext", fmt.Sprintf("%x", blockContext)) } cachedResult, exists := hvmQueryMap[k] if exists { - log.Info(fmt.Sprintf("btcTxConfirmations returning cached result for query of "+ + log.Debug(fmt.Sprintf("btcTxConfirmations returning cached result for query of "+ "%x in context %x, cached result=%x", input, blockContext, cachedResult)) return cachedResult, nil } } - var txid [32]byte + var txid = make([]byte, 32) copy(txid[0:32], input[0:32]) + slices.Reverse(txid) + // txidMade := [32]byte(txid) + txHash := chainhash.Hash{} + err := txHash.SetBytes(txid[:]) + if err != nil { + log.Warn("Unable to lookup tx confirmations by Txid; unable to convert txid %x to chainhash!", "txid", txid, "err", err) + } - blocks, err := TBCIndexer.DB().BlocksByTxId(context.Background(), tbcd.NewTxId(txid)) - if err != nil || blocks == nil || len(blocks) == 0 { - log.Warn("Unable to lookup transaction confirmations by txid", "txid", input) - resp := make([]byte, 0) - hvmQueryMap[k] = resp - return resp, nil + _, blockHash, err := TBCFullNode.TxByTxId(context.Background(), &txHash) + if err != nil { + log.Error("Unable to lookup transaction confirmations by txid", "txid", txid, "err", err) + return nil, err } - // TODO: Canonical check - hash, err := chainhash.NewHash(blocks[0][:]) + // TODO: Canonical check, needs upstream TBC modification. + // For now hVM has an edge case where confirmation value from a forked chain could be returned. + + _, height, err := TBCFullNode.BlockHeaderByHash(context.Background(), blockHash) if err != nil { - log.Warn(fmt.Sprintf("Unable to create blockhash from %x", blocks[0][:])) - resp := make([]byte, 0) - hvmQueryMap[k] = resp - return resp, nil + log.Error(fmt.Sprintf("Unable to get block header by hash %x", blockHash[:])) + return nil, err } - _, height, err := TBCIndexer.BlockHeaderByHash(context.Background(), hash) + heightBest, _, err := TBCFullNode.BlockHeaderBest(context.Background()) + if err != nil { + log.Error("Unable to get best block header") + return nil, err + } resp := make([]byte, 4) - binary.BigEndian.PutUint32(resp, uint32(height)) + binary.BigEndian.PutUint32(resp, uint32(heightBest-height+1)) log.Debug("txidConfirmations returning data", "returnedData", fmt.Sprintf("%x", resp)) @@ -532,6 +930,61 @@ func (c *btcTxConfirmations) Run(input []byte, blockContext common.Hash) ([]byte return resp, nil } +type btcAddrToScript struct{} + +func (c *btcAddrToScript) RequiredGas(input []byte) uint64 { + return params.BtcAddrToScript +} + +func (c *btcAddrToScript) Run(input []byte, blockContext common.Hash) ([]byte, error) { + if input == nil || len(input) < 24 { + log.Debug("btcAddrToScript run called with nil or too small input", "input", fmt.Sprintf("%x", input)) + return nil, nil + } + if TBCFullNode == nil { + log.Crit("TBCIndexer is nil!") + } + + var k hVMQueryKey + if isValidBlock(blockContext) { + k, err := calculateHVMQueryKey(input, hvmContractsToAddress[reflect.TypeOf(c)][0], blockContext) + if err != nil { + log.Error("Unable to calculate hVM Query Key!", + "input", fmt.Sprintf("%x", input), + "blockContext", fmt.Sprintf("%x", blockContext)) + } + cachedResult, exists := hvmQueryMap[k] + if exists { + log.Debug(fmt.Sprintf("btcAddrToScript returning cached result for query of "+ + "%x in context %x, cached result=%x", input, blockContext, cachedResult)) + return cachedResult, nil + } + } + + addressStr := string(input) + log.Debug("btcAddrToScript called", "address", addressStr) + + addr, err := btcutil.DecodeAddress(addressStr, tbcChainParams) + if err != nil { + log.Error("In btcAddrToScript call, unable to decode address", "addressStr", addressStr) + return nil, err + } + + script, err := txscript.PayToAddrScript(addr) + if err != nil { + log.Error("In btcAddrToScript call, unable to convert address to pay script", "addressStr", addressStr) + return nil, err + } + + resp := make([]byte, 0) + resp = append(resp, script[:]...) + log.Debug("btcAddrToScript returning data", "returnedData", fmt.Sprintf("%x", resp)) + if isValidBlock(blockContext) { + hvmQueryMap[k] = resp + } + return resp, nil +} + type btcLastHeader struct{} func (c *btcLastHeader) RequiredGas(input []byte) uint64 { @@ -540,8 +993,7 @@ func (c *btcLastHeader) RequiredGas(input []byte) uint64 { func (c *btcLastHeader) Run(input []byte, blockContext common.Hash) ([]byte, error) { // No input validation - log.Debug("btcLastHeader called") - if TBCIndexer == nil { + if TBCFullNode == nil { log.Crit("TBCIndexer is nil!") } @@ -549,35 +1001,48 @@ func (c *btcLastHeader) Run(input []byte, blockContext common.Hash) ([]byte, err if isValidBlock(blockContext) { k, err := calculateHVMQueryKey(input, hvmContractsToAddress[reflect.TypeOf(c)][0], blockContext) if err != nil { - log.Crit("Unable to calculate hVM Query Key!", "input", input, "blockContext", blockContext) + log.Error("Unable to calculate hVM Query Key!", + "input", fmt.Sprintf("%x", input), + "blockContext", fmt.Sprintf("%x", blockContext)) } cachedResult, exists := hvmQueryMap[k] if exists { - log.Info(fmt.Sprintf("btcTxConfirmations returning cached result for query of "+ + log.Debug(fmt.Sprintf("btcTxConfirmations returning cached result for query of "+ "%x in context %x, cached result=%x", input, blockContext, cachedResult)) return cachedResult, nil } } - height, bestHeader, err := TBCIndexer.BlockHeaderBest(context.Background()) + height, bestHeader, err := TBCFullNode.BlockHeaderBest(context.Background()) if err != nil { - log.Warn("Unable to lookup best header!") - resp := make([]byte, 0) - hvmQueryMap[k] = resp - return resp, nil + log.Error("Unable to lookup best header!") + return nil, err } hash := bestHeader.BlockHash() prevHash := bestHeader.PrevBlock merkle := bestHeader.MerkleRoot + var hashReverse = make([]byte, 32) + copy(hashReverse[0:32], hash[0:32]) + slices.Reverse(hashReverse) + + var prevHashReverse = make([]byte, 32) + copy(prevHashReverse[0:32], prevHash[0:32]) + slices.Reverse(prevHashReverse) + + var merkleReverse = make([]byte, 32) + copy(merkleReverse[0:32], merkle[0:32]) + slices.Reverse(merkleReverse) + + // TODO: serialize header directly instead resp := make([]byte, 4) binary.BigEndian.PutUint32(resp, uint32(height)) - resp = append(resp, hash[:]...) + resp = append(resp, hashReverse[:]...) resp = binary.BigEndian.AppendUint32(resp, uint32(bestHeader.Version)) - resp = append(resp, prevHash[:]...) - resp = append(resp, merkle[:]...) + resp = append(resp, prevHashReverse[:]...) + resp = append(resp, merkleReverse[:]...) resp = binary.BigEndian.AppendUint32(resp, uint32(bestHeader.Timestamp.Unix())) resp = binary.BigEndian.AppendUint32(resp, bestHeader.Bits) resp = binary.BigEndian.AppendUint32(resp, bestHeader.Nonce) @@ -597,14 +1062,17 @@ func (c *btcHeaderN) RequiredGas(input []byte) uint64 { func (c *btcHeaderN) Run(input []byte, blockContext common.Hash) ([]byte, error) { if input == nil || len(input) != 4 { - return nil, nil + log.Debug("btcHeaderN run called with nil or != 4 input", "input", fmt.Sprintf("%x", input)) + return nil, fmt.Errorf("btcHeaderN called with nill or != 4 input") } var k hVMQueryKey if isValidBlock(blockContext) { k, err := calculateHVMQueryKey(input, hvmContractsToAddress[reflect.TypeOf(c)][0], blockContext) if err != nil { - log.Crit("Unable to calculate hVM Query Key!", "input", input, "blockContext", blockContext) + log.Error("Unable to calculate hVM Query Key!", + "input", fmt.Sprintf("%x", input), + "blockContext", fmt.Sprintf("%x", blockContext)) } cachedResult, exists := hvmQueryMap[k] if exists { @@ -620,17 +1088,15 @@ func (c *btcHeaderN) Run(input []byte, blockContext common.Hash) ([]byte, error) uint32(input[3]&0xFF) log.Debug("btcHeaderN called", "height", height) - if TBCIndexer == nil { + if TBCFullNode == nil { log.Crit("TBCIndexer is nil!") } - headers, err := TBCIndexer.BlockHeadersByHeight(context.Background(), uint64(height)) + headers, err := TBCFullNode.BlockHeadersByHeight(context.Background(), uint64(height)) if err != nil || len(headers) == 0 { log.Warn("Unable to lookup header!", "height", height) - resp := make([]byte, 0) - hvmQueryMap[k] = resp - return resp, nil + return nil, nil } // TODO: Canonical check @@ -640,12 +1106,24 @@ func (c *btcHeaderN) Run(input []byte, blockContext common.Hash) ([]byte, error) prevHash := bestHeader.PrevBlock merkle := bestHeader.MerkleRoot + var hashReverse = make([]byte, 32) + copy(hashReverse[0:32], hash[0:32]) + slices.Reverse(hashReverse) + + var prevHashReverse = make([]byte, 32) + copy(prevHashReverse[0:32], prevHash[0:32]) + slices.Reverse(prevHashReverse) + + var merkleReverse = make([]byte, 32) + copy(merkleReverse[0:32], merkle[0:32]) + slices.Reverse(merkleReverse) + resp := make([]byte, 4) binary.BigEndian.PutUint32(resp, uint32(height)) - resp = append(resp, hash[:]...) + resp = append(resp, hashReverse[:]...) resp = binary.BigEndian.AppendUint32(resp, uint32(bestHeader.Version)) - resp = append(resp, prevHash[:]...) - resp = append(resp, merkle[:]...) + resp = append(resp, prevHashReverse[:]...) + resp = append(resp, merkleReverse[:]...) resp = binary.BigEndian.AppendUint32(resp, uint32(bestHeader.Timestamp.Unix())) resp = binary.BigEndian.AppendUint32(resp, bestHeader.Bits) resp = binary.BigEndian.AppendUint32(resp, bestHeader.Nonce) @@ -665,7 +1143,7 @@ func (c *btcUtxosAddrList) RequiredGas(input []byte) uint64 { func (c *btcUtxosAddrList) Run(input []byte, blockContext common.Hash) ([]byte, error) { // TODO: Move to variable, check addr min length + 4 bytes - if len(input) < 27 { + if len(input) < 28 { return nil, nil } @@ -673,7 +1151,9 @@ func (c *btcUtxosAddrList) Run(input []byte, blockContext common.Hash) ([]byte, if isValidBlock(blockContext) { k, err := calculateHVMQueryKey(input, hvmContractsToAddress[reflect.TypeOf(c)][0], blockContext) if err != nil { - log.Crit("Unable to calculate hVM Query Key!", "input", input, "blockContext", blockContext) + log.Error("Unable to calculate hVM Query Key!", + "input", fmt.Sprintf("%x", input), + "blockContext", fmt.Sprintf("%x", blockContext)) } cachedResult, exists := hvmQueryMap[k] if exists { @@ -696,19 +1176,15 @@ func (c *btcUtxosAddrList) Run(input []byte, blockContext common.Hash) ([]byte, log.Debug("btcUtxosAddrList run called", "addr", addr, "pg", pg, "pgSize", pgSize) - if TBCIndexer == nil { + if TBCFullNode == nil { log.Crit("No TBC indexer available, cannot perform hVM precompile call!") } - // TODO: Correct Context - utxos, err := TBCIndexer.UtxosByAddress(context.Background(), addr, uint64(pg), uint64(pgSize)) + utxos, err := TBCFullNode.UtxosByAddress(context.Background(), addr, uint64(pg), uint64(pgSize)) if err != nil { - // TODO: Error handling - log.Crit("Unable to process UTXOs of address %s!", addr) - resp := make([]byte, 0) - hvmQueryMap[k] = resp - return resp, nil + log.Warn("Unable to lookup UTXOs for address!", "addr", addr) + return nil, nil } resp := make([]byte, 1) @@ -749,7 +1225,9 @@ func (c *btcTxByTxid) Run(input []byte, blockContext common.Hash) ([]byte, error if isValidBlock(blockContext) { k, err := calculateHVMQueryKey(input, hvmContractsToAddress[reflect.TypeOf(c)][0], blockContext) if err != nil { - log.Crit("Unable to calculate hVM Query Key!", "input", input, "blockContext", blockContext) + log.Error("Unable to calculate hVM Query Key!", + "input", fmt.Sprintf("%x", input), + "blockContext", fmt.Sprintf("%x", blockContext)) } cachedResult, exists := hvmQueryMap[k] if exists { @@ -774,27 +1252,27 @@ func (c *btcTxByTxid) Run(input []byte, blockContext common.Hash) ([]byte, error includeInputScriptSig := bitflag1&(0x01) != 0 bitflag2 := input[33] - includeInputSeq := bitflag1&(0x01<<7) != 0 + includeInputSeq := bitflag2&(0x01<<7) != 0 includeOutputs := bitflag2&(0x01<<6) != 0 includeOutputScript := bitflag2&(0x01<<5) != 0 includeOutputAddress := bitflag2&(0x01<<4) != 0 - includeOpReturnOutputs := bitflag2&(0x01<<3) != 0 + includeUnspendableOutputs := bitflag2&(0x01<<3) != 0 includeOutputSpent := bitflag2&(0x01<<2) != 0 includeOutputSpentBy := bitflag2&(0x01<<1) != 0 // One unused bit for future, possibly meta-protocol info like Ordinals bitflag3 := input[34] // Gives size limits for data which could get unexpectedly expensive to return // Two free bits here - maxInputsExponent := bitflag3 & (0x07 << 3) // bits xxXXXxxx used as 2^(X), b00=2^0=1, b01=2^1=2, ... up to 2^6=64 inputs - maxOutputsExponent := bitflag3 & (0x07) // bits xxxxxXXX used as 2^(X), b00=2^0=1, b01=2^1=2, ... up to 2^6=64 outputs + maxInputsExponent := (bitflag3 & (0x07 << 3)) >> 3 // bits xxXXXxxx used as 2^(X), b00=2^0=1, b01=2^1=2, ... up to 2^6=64 inputs + maxOutputsExponent := bitflag3 & (0x07) // bits xxxxxXXX used as 2^(X), b00=2^0=1, b01=2^1=2, ... up to 2^6=64 outputs maxInputs := 0x01 << maxInputsExponent maxOutputs := 0x01 << maxOutputsExponent bitflag4 := input[35] // Four free bits here - maxInputScriptSigSizeExponent := bitflag4 & (0x03 << 2) // bits xxxxXXxx used as 2^(4+X), b00=2^(4+0)=16, b01=2^(4+1)=32, ... up to 128 bytes - maxOutputScriptSizeExponent := bitflag4 & (0x03) // bits xxxxxxXX used as 2^(4+X), b00=2^(4+0)=16, b01=2^(4+1)=32, ... up to 128 bytes + maxInputScriptSigSizeExponent := (bitflag4 & (0x03 << 2)) >> 2 // bits xxxxXXxx used as 2^(4+X), b00=2^(4+0)=16, b01=2^(4+1)=32, ... up to 128 bytes + maxOutputScriptSizeExponent := bitflag4 & (0x03) // bits xxxxxxXX used as 2^(4+X), b00=2^(4+0)=16, b01=2^(4+1)=32, ... up to 128 bytes maxInputScriptSigSize := 0x01 << (4 + maxInputScriptSigSizeExponent) maxOutputScriptSize := 0x01 << (4 + maxOutputScriptSizeExponent) @@ -805,36 +1283,22 @@ func (c *btcTxByTxid) Run(input []byte, blockContext common.Hash) ([]byte, error "includeInputSource", includeInputSource, "includeInputScriptSig", includeInputScriptSig, "includeInputSeq", includeInputSeq, "includeInputAddress", includeOutputs, "includeOutputScript", includeOutputScript, "includeOutputAddress", includeOutputAddress, - "includeOpReturnOutputs", includeOpReturnOutputs, "includeOutputSpent", includeOutputSpent, + "includeUnspendableOutputs", includeUnspendableOutputs, "includeOutputSpent", includeOutputSpent, "includeOutputSpentBy", includeOutputSpentBy, "maxInputsExponent", maxInputsExponent, "maxOutputsExponent", maxOutputsExponent, "maxInputScriptSigSizeExponent", maxInputScriptSigSizeExponent, "maxOutputScriptSizeExponent", maxOutputScriptSizeExponent, "maxInputs", maxInputs, "maxOutputs", maxOutputs, "maxInputScriptSigSize", maxInputScriptSigSize, "maxOutputScriptSize", maxOutputScriptSize) - txidMade := [32]byte(txid) - - tx, err := TBCIndexer.TxById(context.Background(), txidMade) - if err != nil { - // TODO: Error handling - log.Warn("Unable to lookup Tx conformations by Txid!", "txid", input) - resp := make([]byte, 0) - hvmQueryMap[k] = resp - return resp, nil - } - + ch := chainhash.Hash{} + err := ch.SetBytes(txid) if err != nil { - // TODO: Error handling - log.Warn("Unable to lookup Tx by Txid!", "txid", txid) - resp := make([]byte, 0) - hvmQueryMap[k] = resp - return resp, nil + log.Warn("Unable to lookup tx by txid; unable to convert txid %x to chainhash", "txid", txid) } - if tx == nil { - // TODO: Error handling - resp := make([]byte, 0) - hvmQueryMap[k] = resp - return resp, nil + tx, block, err := TBCFullNode.TxByTxId(context.Background(), &ch) + if err != nil || tx == nil { + log.Error("Unable to lookup tx by txid", "txid", fmt.Sprintf("%x", txid)) + return nil, nil } resp := make([]byte, 0) @@ -843,16 +1307,12 @@ func (c *btcTxByTxid) Run(input []byte, blockContext common.Hash) ([]byte, error // TODO: Not yet implemented } + // TODO: Canonical check if includeContainingBlock { - blocks, err := TBCIndexer.DB().BlocksByTxId(context.Background(), txidMade) - if err != nil || blocks == nil || len(blocks) == 0 { - // TODO: Error handling - resp := make([]byte, 0) - hvmQueryMap[k] = resp - return resp, nil - } - - resp = append(resp, blocks[0][:]...) + blockHash := make([]byte, 0) + blockHash = append(blockHash, block[:]...) + slices.Reverse(blockHash) + resp = append(resp, blockHash...) } if includeVersion { @@ -860,9 +1320,8 @@ func (c *btcTxByTxid) Run(input []byte, blockContext common.Hash) ([]byte, error } if includeSizes { - // resp = binary.BigEndian.AppendUint32(resp, tx.Serialize()) - // resp = binary.BigEndian.AppendUint32(resp, tx.VSize) - // TODO + resp = binary.BigEndian.AppendUint32(resp, uint32(tx.SerializeSize())) + resp = binary.BigEndian.AppendUint32(resp, uint32(tx.SerializeSizeStripped())) } if includeLockTime { @@ -870,19 +1329,28 @@ func (c *btcTxByTxid) Run(input []byte, blockContext common.Hash) ([]byte, error } if includeInputs { - resp = append(resp, byte(len(tx.TxIn))) // TODO: Check no more inputs than allowed - for _, in := range tx.TxIn { + resp = binary.BigEndian.AppendUint16(resp, uint16(len(tx.TxIn))) + for count, in := range tx.TxIn { + if count >= maxInputs { + // Caller needs to check # of inputs compared to claimed length to detect inputs were chopped + break + } // Always include input value - Review if this is desired behavior because of extra lookup cost prevIn := in.PreviousOutPoint - sourceTx, err := TBCIndexer.TxById(context.Background(), tbcd.NewTxId(prevIn.Hash)) - - value := sourceTx.TxOut[prevIn.Index].Value + pih := chainhash.Hash{} + err := pih.SetBytes(prevIn.Hash[:]) + if err != nil { + log.Warn("Unable to lookup Tx by Txid; unable to convert txid %x to chainhash!", "txid", txid) + return nil, nil + } + sourceTx, _, err := TBCFullNode.TxByTxId(context.Background(), &pih) if err != nil { - resp := make([]byte, 0) - hvmQueryMap[k] = resp - return resp, nil + log.Warn("unable to lookup input transaction", + "prevInTxID", fmt.Sprintf("%x", prevIn.Hash), "prevInTxIndex", prevIn.Index) + return nil, nil } + value := sourceTx.TxOut[prevIn.Index].Value resp = binary.BigEndian.AppendUint64(resp, uint64(value)) if includeInputSource { @@ -892,9 +1360,13 @@ func (c *btcTxByTxid) Run(input []byte, blockContext common.Hash) ([]byte, error resp = binary.BigEndian.AppendUint16(resp, uint16(prevIn.Index)) // TODO: Check outputs cannot exceed 2^16-1 } if includeInputScriptSig { - // TODO: chop to max size and decide on size encoding + choppedInputScript := make([]byte, 0) + choppedInputScript = append(choppedInputScript, in.SignatureScript...) + if len(choppedInputScript) > maxInputScriptSigSize { + choppedInputScript = choppedInputScript[0:maxInputScriptSigSize] + } resp = binary.BigEndian.AppendUint16(resp, uint16(len(in.SignatureScript))) - resp = append(resp, in.SignatureScript...) + resp = append(resp, choppedInputScript...) } // // TODO: respect max inputs setting @@ -913,21 +1385,30 @@ func (c *btcTxByTxid) Run(input []byte, blockContext common.Hash) ([]byte, error } outLen := len(tx.TxOut) - if !includeOpReturnOutputs { + if !includeUnspendableOutputs { outLen -= unspendable } - resp = append(resp, byte(outLen)) // TODO: Check no more outputs than allowed + count := 0 + resp = binary.BigEndian.AppendUint16(resp, uint16(outLen)) for idx, out := range tx.TxOut { - // Always include output value + if count >= maxOutputs { + // Caller needs to check # of outputs compared to claimed length to detect outputs were chopped + break + } + unspendable := txscript.IsUnspendable(out.PkScript) + if unspendable && !includeUnspendableOutputs { + continue + } resp = binary.BigEndian.AppendUint64(resp, uint64(out.Value)) if includeOutputScript { - unspendable := txscript.IsUnspendable(out.PkScript) - if unspendable && !includeOpReturnOutputs { - continue + choppedOutputScript := make([]byte, 0) + choppedOutputScript = append(choppedOutputScript, out.PkScript...) + if len(choppedOutputScript) > maxOutputScriptSize { + choppedOutputScript = choppedOutputScript[0:maxOutputScriptSize] } - resp = append(resp, byte(len(out.PkScript))) // TODO: Length check and truncate - resp = append(resp, out.PkScript...) + resp = binary.BigEndian.AppendUint16(resp, uint16(len(out.PkScript))) + resp = append(resp, choppedOutputScript...) } if includeOutputAddress { // TODO @@ -936,16 +1417,20 @@ func (c *btcTxByTxid) Run(input []byte, blockContext common.Hash) ([]byte, error // resp = append(resp, addrBytes...) // TODO: right now this is just ASCII->Bytes, consider changing to Base58 decode? Could be flag option } if includeOutputSpent { - op := tbcd.NewOutpoint(txidMade, uint32(idx)) - sh, _ := TBCIndexer.DB().ScriptHashByOutpoint(context.Background(), op) + spentBool, err := TBCFullNode.ScriptHashAvailableToSpend(context.Background(), &ch, uint32(idx)) + + if err != nil { + log.Warn("Unable to lookup output spend status", "txid", txid) + return nil, nil + } - spent := 0 - if sh == nil { + spent := byte(0) + if spentBool { // Could not look up Outpoint in UTXO table, therefore spent - spent = 1 + spent = byte(1) } - resp = append(resp, byte(spent)) + resp = append(resp, spent) if includeOutputSpentBy { // If not spent, do not include spender TxID if spent == 1 { @@ -956,6 +1441,7 @@ func (c *btcTxByTxid) Run(input []byte, blockContext common.Hash) ([]byte, error } } } + count++ } } diff --git a/core/vm/contracts_fuzz_test.go b/core/vm/contracts_fuzz_test.go index 87c1fff7cc..b91716ec33 100644 --- a/core/vm/contracts_fuzz_test.go +++ b/core/vm/contracts_fuzz_test.go @@ -36,7 +36,7 @@ func FuzzPrecompiledContracts(f *testing.F) { return } inWant := string(input) - RunPrecompiledContract(p, input, gas) + RunPrecompiledContract(p, input, gas, common.Hash{0}) // TODO: Review whether this works with hVM if inHave := string(input); inWant != inHave { t.Errorf("Precompiled %v modified input data", a) } diff --git a/core/vm/contracts_test.go b/core/vm/contracts_test.go index f40e2c8f9e..759551590c 100644 --- a/core/vm/contracts_test.go +++ b/core/vm/contracts_test.go @@ -66,7 +66,7 @@ var allPrecompiles = map[common.Address]PrecompiledContract{ common.BytesToAddress([]byte{0x0f, 0x0f}): &bls12381G2MultiExp{}, common.BytesToAddress([]byte{0x0f, 0x10}): &bls12381Pairing{}, common.BytesToAddress([]byte{0x0f, 0x11}): &bls12381MapG1{}, - common.BytesToAddress([]byte{0x0f, 0x12}): &bls12381MapG2{}, + common.BytesToAddress([]byte{0x0f, 0x12}): &bls12381MapG2{}, // TODO MAX: hVM Tests Here? } // EIP-152 test vectors @@ -98,7 +98,7 @@ func testPrecompiled(addr string, test precompiledTest, t *testing.T) { in := common.Hex2Bytes(test.Input) gas := p.RequiredGas(in) t.Run(fmt.Sprintf("%s-Gas=%d", test.Name, gas), func(t *testing.T) { - if res, _, err := RunPrecompiledContract(p, in, gas); err != nil { + if res, _, err := RunPrecompiledContract(p, in, gas, common.Hash{0}); err != nil { t.Error(err) } else if common.Bytes2Hex(res) != test.Expected { t.Errorf("Expected %v, got %v", test.Expected, common.Bytes2Hex(res)) @@ -120,7 +120,7 @@ func testPrecompiledOOG(addr string, test precompiledTest, t *testing.T) { gas := p.RequiredGas(in) - 1 t.Run(fmt.Sprintf("%s-Gas=%d", test.Name, gas), func(t *testing.T) { - _, _, err := RunPrecompiledContract(p, in, gas) + _, _, err := RunPrecompiledContract(p, in, gas, common.Hash{0}) if err.Error() != "out of gas" { t.Errorf("Expected error [out of gas], got [%v]", err) } @@ -137,7 +137,7 @@ func testPrecompiledFailure(addr string, test precompiledFailureTest, t *testing in := common.Hex2Bytes(test.Input) gas := p.RequiredGas(in) t.Run(test.Name, func(t *testing.T) { - _, _, err := RunPrecompiledContract(p, in, gas) + _, _, err := RunPrecompiledContract(p, in, gas, common.Hash{0}) if err.Error() != test.ExpectedError { t.Errorf("Expected error [%v], got [%v]", test.ExpectedError, err) } @@ -169,7 +169,7 @@ func benchmarkPrecompiled(addr string, test precompiledTest, bench *testing.B) { bench.ResetTimer() for i := 0; i < bench.N; i++ { copy(data, in) - res, _, err = RunPrecompiledContract(p, data, reqGas) + res, _, err = RunPrecompiledContract(p, data, reqGas, common.Hash{0}) } bench.StopTimer() elapsed := uint64(time.Since(start)) diff --git a/core/vm/evm.go b/core/vm/evm.go index aa17eaaa2a..a155b4a73c 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -55,6 +55,13 @@ func (evm *EVM) precompile(addr common.Address) (PrecompiledContract, bool) { default: precompiles = PrecompiledContractsHomestead } + + if evm.chainRules.IsHvm0 { + for address, contract := range PrecompiledContractsHvm0 { + precompiles[address] = contract + } + } + p, ok := precompiles[addr] // Restrict overrides to known precompiles if ok && evm.chainConfig.IsOptimism() && evm.Config.OptimismPrecompileOverrides != nil { diff --git a/eth/backend.go b/eth/backend.go index 4d427b583c..b2a3c207a0 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -21,6 +21,8 @@ import ( "context" "errors" "fmt" + "github.com/hemilabs/heminetwork/cmd/btctool/bdf" + "github.com/hemilabs/heminetwork/service/tbc" "math/big" "runtime" "sync" @@ -229,8 +231,34 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { if config.OverrideOptimismInterop != nil { overrides.OverrideOptimismInterop = config.OverrideOptimismInterop } + if config.OverrideHemiHvm0 != nil { + overrides.OverrideHemiHvm0 = config.OverrideHemiHvm0 + log.Info("Creating new blockchain with hVM0 override set to: %d", overrides.OverrideHemiHvm0) + } else { + log.Info("Creating new blockchain, hVM0 override not set.") + } + overrides.ApplySuperchainUpgrades = config.ApplySuperchainUpgrades eth.blockchain, err = core.NewBlockChain(chainDb, cacheConfig, config.Genesis, &overrides, eth.engine, vmConfig, eth.shouldPreserve, &config.TransactionHistory) + + if config.HvmEnabled { + tbcCfg := tbc.NewDefaultConfig() + + genesisHeader, err := bdf.Hex2Header(config.HvmGenesisHeader) + if err != nil { + log.Crit("Unable to deserialize hVM Genesis Header", "header", config.HvmGenesisHeader, "err", err) + } + + tbcCfg.ExternalHeaderMode = true + tbcCfg.EffectiveGenesisBlock = genesisHeader + tbcCfg.GenesisHeightOffset = config.HvmGenesisHeight + tbcCfg.LevelDBHome = config.HvmHeaderDataDir + + // TODO: Pull from chain config, each Hemi chain should be configured with a corresponding BTC net + tbcCfg.Network = "testnet3" + + eth.blockchain.SetupHvmHeaderNode(tbcCfg) + } if err != nil { return nil, err } diff --git a/eth/ethconfig/config.go b/eth/ethconfig/config.go index afe26e37a8..032ea47e28 100644 --- a/eth/ethconfig/config.go +++ b/eth/ethconfig/config.go @@ -68,6 +68,12 @@ var Defaults = Config{ RPCEVMTimeout: 5 * time.Second, GPO: FullNodeGPO, RPCTxFeeCap: 1, // 1 ether + // TODO: Move hVM defaults somewhere else on a per-network basis + HvmEnabled: true, + HvmGenesisHeader: "0040f72e185e726ab36562d067c557e57d4e6f4c2fdc13123d5e983002000000000000009d54e40d199819344b832ef6c34ca3d959bc0df6a0d91918ede13fd9ef45fbe245599866d9f119196eca2d5a", + HvmGenesisHeight: 2868779, + HvmHeaderDataDir: "~/.tbcdheaders", // TODO: put this in configured geth data directory + } //go:generate go run github.com/fjl/gencodec -type Config -formats toml -out gen_config.go @@ -155,6 +161,11 @@ type Config struct { // send-transaction variants. The unit is ether. RPCTxFeeCap float64 + HvmEnabled bool `toml:",omitempty"` + HvmGenesisHeader string `toml:",omitempty"` + HvmGenesisHeight uint64 `toml:",omitempty"` + HvmHeaderDataDir string `toml:",omitempty"` + // OverrideCancun (TODO: remove after the fork) OverrideCancun *uint64 `toml:",omitempty"` @@ -167,6 +178,13 @@ type Config struct { OverrideOptimismInterop *uint64 `toml:",omitempty"` + OverrideHemiHvm0 *uint64 `toml:",omitempty"` + + OverrideHvmEnabled bool `toml:",omitempty"` + OverrideHvmGenesisHeader string `toml:",omitempty"` + OverrideHvmGenesisHeight *uint64 `toml:",omitempty"` + OverrideHvmHeaderDataDir string `toml:",omitempty"` + // ApplySuperchainUpgrades requests the node to load chain-configuration from the superchain-registry. ApplySuperchainUpgrades bool `toml:",omitempty"` diff --git a/eth/ethconfig/gen_config.go b/eth/ethconfig/gen_config.go index 92a7147396..7385cc6b2d 100644 --- a/eth/ethconfig/gen_config.go +++ b/eth/ethconfig/gen_config.go @@ -54,11 +54,20 @@ func (c Config) MarshalTOML() (interface{}, error) { RPCGasCap uint64 RPCEVMTimeout time.Duration RPCTxFeeCap float64 + HvmEnabled bool `toml:",omitempty"` + HvmGenesisHeader string `toml:",omitempty"` + HvmGenesisHeight uint64 `toml:",omitempty"` + HvmHeaderDataDir string `toml:",omitempty"` OverrideCancun *uint64 `toml:",omitempty"` OverrideVerkle *uint64 `toml:",omitempty"` OverrideOptimismCanyon *uint64 `toml:",omitempty"` OverrideOptimismEcotone *uint64 `toml:",omitempty"` OverrideOptimismInterop *uint64 `toml:",omitempty"` + OverrideHemiHvm0 *uint64 `toml:",omitempty"` + OverrideHvmEnabled bool `toml:",omitempty"` + OverrideHvmGenesisHeader string `toml:",omitempty"` + OverrideHvmGenesisHeight *uint64 `toml:",omitempty"` + OverrideHvmHeaderDataDir string `toml:",omitempty"` ApplySuperchainUpgrades bool `toml:",omitempty"` RollupSequencerHTTP string RollupHistoricalRPC string @@ -105,11 +114,20 @@ func (c Config) MarshalTOML() (interface{}, error) { enc.RPCGasCap = c.RPCGasCap enc.RPCEVMTimeout = c.RPCEVMTimeout enc.RPCTxFeeCap = c.RPCTxFeeCap + enc.HvmEnabled = c.HvmEnabled + enc.HvmGenesisHeader = c.HvmGenesisHeader + enc.HvmGenesisHeight = c.HvmGenesisHeight + enc.HvmHeaderDataDir = c.HvmHeaderDataDir enc.OverrideCancun = c.OverrideCancun enc.OverrideVerkle = c.OverrideVerkle enc.OverrideOptimismCanyon = c.OverrideOptimismCanyon enc.OverrideOptimismEcotone = c.OverrideOptimismEcotone enc.OverrideOptimismInterop = c.OverrideOptimismInterop + enc.OverrideHemiHvm0 = c.OverrideHemiHvm0 + enc.OverrideHvmEnabled = c.OverrideHvmEnabled + enc.OverrideHvmGenesisHeader = c.OverrideHvmGenesisHeader + enc.OverrideHvmGenesisHeight = c.OverrideHvmGenesisHeight + enc.OverrideHvmHeaderDataDir = c.OverrideHvmHeaderDataDir enc.ApplySuperchainUpgrades = c.ApplySuperchainUpgrades enc.RollupSequencerHTTP = c.RollupSequencerHTTP enc.RollupHistoricalRPC = c.RollupHistoricalRPC @@ -160,11 +178,20 @@ func (c *Config) UnmarshalTOML(unmarshal func(interface{}) error) error { RPCGasCap *uint64 RPCEVMTimeout *time.Duration RPCTxFeeCap *float64 + HvmEnabled *bool `toml:",omitempty"` + HvmGenesisHeader *string `toml:",omitempty"` + HvmGenesisHeight *uint64 `toml:",omitempty"` + HvmHeaderDataDir *string `toml:",omitempty"` OverrideCancun *uint64 `toml:",omitempty"` OverrideVerkle *uint64 `toml:",omitempty"` OverrideOptimismCanyon *uint64 `toml:",omitempty"` OverrideOptimismEcotone *uint64 `toml:",omitempty"` OverrideOptimismInterop *uint64 `toml:",omitempty"` + OverrideHemiHvm0 *uint64 `toml:",omitempty"` + OverrideHvmEnabled *bool `toml:",omitempty"` + OverrideHvmGenesisHeader *string `toml:",omitempty"` + OverrideHvmGenesisHeight *uint64 `toml:",omitempty"` + OverrideHvmHeaderDataDir *string `toml:",omitempty"` ApplySuperchainUpgrades *bool `toml:",omitempty"` RollupSequencerHTTP *string RollupHistoricalRPC *string @@ -288,6 +315,18 @@ func (c *Config) UnmarshalTOML(unmarshal func(interface{}) error) error { if dec.RPCTxFeeCap != nil { c.RPCTxFeeCap = *dec.RPCTxFeeCap } + if dec.HvmEnabled != nil { + c.HvmEnabled = *dec.HvmEnabled + } + if dec.HvmGenesisHeader != nil { + c.HvmGenesisHeader = *dec.HvmGenesisHeader + } + if dec.HvmGenesisHeight != nil { + c.HvmGenesisHeight = *dec.HvmGenesisHeight + } + if dec.HvmHeaderDataDir != nil { + c.HvmHeaderDataDir = *dec.HvmHeaderDataDir + } if dec.OverrideCancun != nil { c.OverrideCancun = dec.OverrideCancun } @@ -303,6 +342,21 @@ func (c *Config) UnmarshalTOML(unmarshal func(interface{}) error) error { if dec.OverrideOptimismInterop != nil { c.OverrideOptimismInterop = dec.OverrideOptimismInterop } + if dec.OverrideHemiHvm0 != nil { + c.OverrideHemiHvm0 = dec.OverrideHemiHvm0 + } + if dec.OverrideHvmEnabled != nil { + c.OverrideHvmEnabled = *dec.OverrideHvmEnabled + } + if dec.OverrideHvmGenesisHeader != nil { + c.OverrideHvmGenesisHeader = *dec.OverrideHvmGenesisHeader + } + if dec.OverrideHvmGenesisHeight != nil { + c.OverrideHvmGenesisHeight = dec.OverrideHvmGenesisHeight + } + if dec.OverrideHvmHeaderDataDir != nil { + c.OverrideHvmHeaderDataDir = *dec.OverrideHvmHeaderDataDir + } if dec.ApplySuperchainUpgrades != nil { c.ApplySuperchainUpgrades = *dec.ApplySuperchainUpgrades } diff --git a/go.mod b/go.mod index 4464198603..f01647d423 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/route53 v1.30.2 github.com/btcsuite/btcd v0.24.0 github.com/btcsuite/btcd/btcec/v2 v2.3.2 + github.com/btcsuite/btcd/btcutil v1.1.5 github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 github.com/cespare/cp v0.1.0 github.com/cloudflare/cloudflare-go v0.79.0 @@ -97,7 +98,6 @@ require ( github.com/aws/smithy-go v1.15.0 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bits-and-blooms/bitset v1.10.0 // indirect - github.com/btcsuite/btcd/btcutil v1.1.5 // indirect github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/cockroachdb/errors v1.8.1 // indirect diff --git a/go.sum b/go.sum index 4167f30ee5..5a9b8fd7ff 100644 --- a/go.sum +++ b/go.sum @@ -314,8 +314,6 @@ github.com/hashicorp/go-retryablehttp v0.7.4 h1:ZQgVdpTdAL7WpMIwLzCfbalOcSUdkDZn github.com/hashicorp/go-retryablehttp v0.7.4/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hemilabs/heminetwork v0.2.0 h1:aUSIPpti6WvBkVcpTnZCXhu/Av/yCFlSeMw5sBPIqMc= -github.com/hemilabs/heminetwork v0.2.0/go.mod h1:Te/IuHkkV4hyAzmweCTbNu/7piMeFIIIojbRV2iJ034= github.com/holiman/billy v0.0.0-20230718173358-1c7e68d277a7 h1:3JQNjnMRil1yD0IfZKHF9GxxWKDJGj8I0IqOUol//sw= github.com/holiman/billy v0.0.0-20230718173358-1c7e68d277a7/go.mod h1:5GuXa7vkL8u9FkFuWdVvfR5ix8hRB7DbOAaYULamFpc= github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao= diff --git a/miner/payload_building.go b/miner/payload_building.go index 0190496c59..e70446e3a2 100644 --- a/miner/payload_building.go +++ b/miner/payload_building.go @@ -260,6 +260,7 @@ func (w *worker) buildPayload(args *BuildPayloadArgs) (*Payload, error) { // enough to run. The empty payload can at least make sure there is something // to deliver for not missing slot. // In OP-Stack, the "empty" block is constructed from provided txs only, i.e. no tx-pool usage. + // For hVM, the "empty" block will not include any BTC Attributes Deposited transaction either. emptyParams := &generateParams{ timestamp: args.Timestamp, forceTime: true, diff --git a/miner/worker.go b/miner/worker.go index 8fe96f9a6c..1dc25f5733 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -869,6 +869,7 @@ func (w *worker) commitTransactions(env *environment, txs *transactionsByPriceAn txs.Pop() continue } + // Transaction seems to fit, pull it up from the pool tx := ltx.Resolve() if tx == nil { @@ -876,6 +877,17 @@ func (w *worker) commitTransactions(env *environment, txs *transactionsByPriceAn txs.Pop() continue } + + // Ensure transaction isn't a Bitcoin Attributes Deposited or PoP Payout tx, since they should never come from mempool + if tx.IsBtcAttributesDepositedTx() { + log.Error("Rejected a Bitcoin Attributes Deposited transaction that was in the mempool.") + txs.Pop() + } + if tx.IsPopPayoutTx() { + log.Error("Rejected a PoP Payout transaction that was in the mempool.") + txs.Pop() + } + // Error may be ignored here. The error has already been checked // during transaction acceptance is the transaction pool. from, _ := types.Sender(env.signer, tx) @@ -1122,6 +1134,27 @@ func (w *worker) generateWork(genParams *generateParams) *newPayloadResult { // forced transactions done, fill rest of block with transactions if !genParams.noTxs { + // First, check whether a new Bitcoin Attributes Deposited tx should be included. + // This is a redundant check since GetBitcoinAttributesForNextBlock will return nil with no error if hVM is not enabled/activated. + if w.eth.BlockChain().IsHvmEnabled() && w.chainConfig.IsHvm0(genParams.timestamp) { + btcAttrDepTx, err := w.eth.BlockChain().GetBitcoinAttributesForNextBlock(work.header.Time) + if err != nil { + log.Error("Failed to create a Bitcoin Attributes Deposited transaction in generateWork()", "err", err) + } + if btcAttrDepTx != nil { + cast := types.NewTx(btcAttrDepTx) + from, _ := types.Sender(work.signer, cast) + work.state.SetTxContext(cast.Hash(), work.tcount) + _, err := w.commitTransaction(work, cast) + if err != nil { + return &newPayloadResult{err: fmt.Errorf("failed to force-include Bitcoin Attributes Deposited tx: %s type: %d sender: %s nonce: %d, err: %w", cast.Hash(), cast.Type(), from, cast.Nonce(), err)} + } + work.tcount++ + } + } else { + log.Info("worker not generating a Bitcoin Attributes Deposited transaction") + } + // use shared interrupt if present interrupt := genParams.interrupt if interrupt == nil { @@ -1131,7 +1164,7 @@ func (w *worker) generateWork(genParams *generateParams) *newPayloadResult { interrupt.Store(commitInterruptTimeout) }) - err := w.fillTransactions(interrupt, work) + err = w.fillTransactions(interrupt, work) timer.Stop() // don't need timeout interruption any more if errors.Is(err, errBlockInterruptedByTimeout) { log.Warn("Block building is interrupted", "allowance", common.PrettyDuration(w.newpayloadTimeout)) diff --git a/params/config.go b/params/config.go index e7b3c0494b..63a757fcd8 100644 --- a/params/config.go +++ b/params/config.go @@ -396,6 +396,9 @@ type ChainConfig struct { InteropTime *uint64 `json:"interopTime,omitempty"` // Interop switch time (nil = no fork, 0 = already on optimism interop) + // Hemi-specific activations + Hvm0Time *uint64 `json:"hvm0Time,omitempty"` // HVM Phase 0 activation time + // TerminalTotalDifficulty is the amount of total difficulty reached by // the network that triggers the consensus upgrade. TerminalTotalDifficulty *big.Int `json:"terminalTotalDifficulty,omitempty"` @@ -547,6 +550,9 @@ func (c *ChainConfig) Description() string { if c.InteropTime != nil { banner += fmt.Sprintf(" - Interop: @%-10v\n", *c.InteropTime) } + if c.Hvm0Time != nil { + banner += fmt.Sprintf(" - HVM Phase 0: @%-10v\n", *c.Hvm0Time) + } return banner } @@ -698,6 +704,10 @@ func (c *ChainConfig) IsOptimismPreBedrock(num *big.Int) bool { return c.IsOptimism() && !c.IsBedrock(num) } +func (c *ChainConfig) IsHvm0(time uint64) bool { + return isTimestampForked(c.Hvm0Time, time) +} + // CheckCompatible checks whether scheduled fork transitions have been imported // with a mismatching chain configuration. func (c *ChainConfig) CheckCompatible(newcfg *ChainConfig, height uint64, time uint64) *ConfigCompatError { @@ -1034,6 +1044,7 @@ type Rules struct { IsVerkle bool IsOptimismBedrock, IsOptimismRegolith bool IsOptimismCanyon bool + IsHvm0 bool } // Rules ensures c's ChainID is not nil. @@ -1065,5 +1076,7 @@ func (c *ChainConfig) Rules(num *big.Int, isMerge bool, timestamp uint64) Rules IsOptimismBedrock: isMerge && c.IsOptimismBedrock(num), IsOptimismRegolith: isMerge && c.IsOptimismRegolith(timestamp), IsOptimismCanyon: isMerge && c.IsOptimismCanyon(timestamp), + // Hemi + IsHvm0: c.IsHvm0(timestamp), // TODO: Review HVM's compatibility with older upgrades and require minimum one here } } diff --git a/params/protocol_params.go b/params/protocol_params.go index 84f093f42b..d1a6f2f0cb 100644 --- a/params/protocol_params.go +++ b/params/protocol_params.go @@ -173,6 +173,7 @@ const ( BtcLastHeader uint64 = 5000 BtcHeaderN uint64 = 6000 BtcFeesLastBlock uint64 = 10000 + BtcAddrToScript uint64 = 25000 // The Refund Quotient is the cap on how much of the used gas can be refunded. Before EIP-3529, // up to half the consumed gas could be refunded. Redefined as 1/5th in EIP-3529