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