From 71f203feccefd65ad6a4dd0fff136fa68c78f13f Mon Sep 17 00:00:00 2001 From: Jonathan Downing Date: Wed, 11 Dec 2024 19:39:33 -0600 Subject: [PATCH] Added a precompile that stores locked coinbases compressed into tranches --- cmd/utils/hierarchical_coordinator.go | 4 +- core/rawdb/accessors_chain.go | 65 ++++- core/rawdb/schema.go | 12 + core/rawdb/table.go | 6 + core/state/statedb.go | 38 ++- core/state_processor.go | 401 +++++++++++++++----------- core/types/transaction.go | 1 + core/types/utxo.go | 6 + core/vm/contracts.go | 236 ++++++++++++++- core/vm/evm.go | 51 +++- core/vm/interface.go | 6 + core/worker.go | 362 +++++++++++++++-------- ethdb/batch.go | 4 + ethdb/leveldb/leveldb.go | 43 ++- ethdb/memorydb/memorydb.go | 6 + ethdb/pebble/pebble.go | 6 + params/protocol_params.go | 4 +- quai/api_backend.go | 2 +- 18 files changed, 933 insertions(+), 320 deletions(-) diff --git a/cmd/utils/hierarchical_coordinator.go b/cmd/utils/hierarchical_coordinator.go index 6246d29479..0fc96b0a96 100644 --- a/cmd/utils/hierarchical_coordinator.go +++ b/cmd/utils/hierarchical_coordinator.go @@ -981,7 +981,7 @@ func (hc *HierarchicalCoordinator) BuildPendingHeaders(wo *types.WorkObject, ord log.Global.Debug("Entropy: ", common.BigBitsToBits(entropy)) nodeSet, exists := hc.Get(entropy) if !exists { - log.Global.WithFields(log.Fields{"entropy": common.BigBitsToBits(entropy), "order": order, "number": wo.NumberArray(), "hash": wo.Hash()}).Debug("NodeSet not found for entropy") + log.Global.WithFields(log.Fields{"entropy": common.BigBitsToBits(entropy), "order": order, "number": wo.NumberArray(), "hash": wo.Hash()}).Trace("NodeSet not found for entropy") } if nodeSet.Extendable(wo, order) { @@ -997,7 +997,7 @@ func (hc *HierarchicalCoordinator) BuildPendingHeaders(wo *types.WorkObject, ord } hc.Add(newSetEntropy, newNodeSet, newPendingHeaders) } else { - log.Global.WithFields(log.Fields{"entropy": common.BigBitsToBits(entropy), "order": order, "number": wo.NumberArray(), "hash": wo.Hash()}).Debug("NodeSet not found for entropy") + log.Global.WithFields(log.Fields{"entropy": common.BigBitsToBits(entropy), "order": order, "number": wo.NumberArray(), "hash": wo.Hash()}).Trace("NodeSet not found for entropy") } misses++ if misses > threshold { diff --git a/core/rawdb/accessors_chain.go b/core/rawdb/accessors_chain.go index 156d814c07..44bc07996f 100644 --- a/core/rawdb/accessors_chain.go +++ b/core/rawdb/accessors_chain.go @@ -24,9 +24,9 @@ import ( "github.com/dominant-strategies/go-quai/common" "github.com/dominant-strategies/go-quai/core/types" + "github.com/dominant-strategies/go-quai/crypto/multiset" "github.com/dominant-strategies/go-quai/ethdb" "github.com/dominant-strategies/go-quai/log" - "github.com/dominant-strategies/go-quai/crypto/multiset" "github.com/dominant-strategies/go-quai/params" "google.golang.org/protobuf/proto" ) @@ -1710,3 +1710,66 @@ func DeleteUtxoToBlockHeight(db ethdb.KeyValueWriter, txHash common.Hash, index db.Logger().WithField("err", err).Fatal("Failed to delete utxo to block height") } } + +func ReadCoinbaseLockup(db ethdb.KeyValueReader, batch ethdb.Batch, ownerContract common.Address, beneficiaryMiner common.Address, lockupByte byte, epoch uint32) (*big.Int, uint32, uint16) { + deleted, data := batch.GetPending(CoinbaseLockupKey(ownerContract, beneficiaryMiner, lockupByte, epoch)) + if deleted { + return new(big.Int), 0, 0 + } else if data != nil { + amount := new(big.Int).SetBytes(data[:32]) + blockHeight := binary.BigEndian.Uint32(data[32:36]) + elements := binary.BigEndian.Uint16(data[36:38]) + return amount, blockHeight, elements + } + // If the data is not in the batch, try to look up the data in leveldb + data, _ = db.Get(CoinbaseLockupKey(ownerContract, beneficiaryMiner, lockupByte, epoch)) + if len(data) == 0 { + return new(big.Int), 0, 0 + } + amount := new(big.Int).SetBytes(data[:32]) + blockHeight := binary.BigEndian.Uint32(data[32:36]) + elements := binary.BigEndian.Uint16(data[36:38]) + return amount, blockHeight, elements +} + +func WriteCoinbaseLockup(db ethdb.KeyValueWriter, ownerContract common.Address, beneficiaryMiner common.Address, lockupByte byte, epoch uint32, amount *big.Int, blockHeight uint32, elements uint16) ([]byte, error) { + data := make([]byte, 38) + amountBytes := amount.Bytes() + if len(amountBytes) > 32 { + return nil, fmt.Errorf("amount is too large") + } + // Right-align amountBytes in data[:32] + copy(data[32-len(amountBytes):32], amountBytes) + binary.BigEndian.PutUint32(data[32:36], blockHeight) + binary.BigEndian.PutUint16(data[36:38], elements) + key := CoinbaseLockupKey(ownerContract, beneficiaryMiner, lockupByte, epoch) + if err := db.Put(key, data); err != nil { + db.Logger().WithField("err", err).Fatal("Failed to store coinbase lockup") + } + return key, nil +} + +func WriteCoinbaseLockupToMap(coinbaseMap map[[47]byte][]byte, key [47]byte, amount *big.Int, blockHeight uint32, elements uint16) error { + data := make([]byte, 38) + amountBytes := amount.Bytes() + if len(amountBytes) > 32 { + return fmt.Errorf("amount is too large") + } + // Right-align amountBytes in data[:32] + copy(data[32-len(amountBytes):32], amountBytes) + binary.BigEndian.PutUint32(data[32:36], blockHeight) + binary.BigEndian.PutUint16(data[36:38], elements) + coinbaseMap[[47]byte(key)] = data + return nil +} + +func DeleteCoinbaseLockup(db ethdb.KeyValueWriter, ownerContract common.Address, beneficiaryMiner common.Address, lockupByte byte, epoch uint32) [47]byte { + key := CoinbaseLockupKey(ownerContract, beneficiaryMiner, lockupByte, epoch) + if err := db.Delete(key); err != nil { + db.Logger().WithField("err", err).Fatal("Failed to delete coinbase lockup") + } + if len(key) != 47 { + db.Logger().Fatal("CoinbaseLockupKey is not 47 bytes") + } + return [47]byte(key) +} diff --git a/core/rawdb/schema.go b/core/rawdb/schema.go index 8a89753ebc..fad383c705 100644 --- a/core/rawdb/schema.go +++ b/core/rawdb/schema.go @@ -114,6 +114,7 @@ var ( // Chain index prefixes (use `i` + single byte to avoid mixing data types). BloomBitsIndexPrefix = []byte("iB") // BloomBitsIndexPrefix is the data table of a chain indexer to track its progress + CoinbaseLockupPrefix = []byte("cl") // coinbaseLockupPrefix + ownerContract + beneficiaryMiner + lockupByte + epoch -> lockup ) const ( @@ -387,3 +388,14 @@ func utxoToBlockHeightKey(txHash common.Hash, index uint16) []byte { txHash[common.HashLength-2] = indexBytes[1] return append(utxoToBlockHeightPrefix, txHash[:]...) } + +func CoinbaseLockupKey(ownerContract common.Address, beneficiaryMiner common.Address, lockupByte byte, epoch uint32) []byte { + epochBytes := make([]byte, 4) + binary.BigEndian.PutUint32(epochBytes, epoch) + ownerBytes := ownerContract.Bytes() + beneficiaryBytes := beneficiaryMiner.Bytes() + combined := append(ownerBytes, beneficiaryBytes...) + combined = append(combined, lockupByte) + combined = append(combined, epochBytes...) + return append(CoinbaseLockupPrefix, combined...) +} diff --git a/core/rawdb/table.go b/core/rawdb/table.go index 30219f3876..cafcae9b35 100644 --- a/core/rawdb/table.go +++ b/core/rawdb/table.go @@ -241,6 +241,12 @@ func (b *tableBatch) Replay(w ethdb.KeyValueWriter) error { return b.batch.Replay(&tableReplayer{w: w, prefix: b.prefix}) } +func (b *tableBatch) SetPending(pending bool) {} + +func (b *tableBatch) GetPending(key []byte) (bool, []byte) { + return false, nil +} + // tableIterator is a wrapper around a database iterator that prefixes each key access // with a pre-configured string. type tableIterator struct { diff --git a/core/state/statedb.go b/core/state/statedb.go index 8918f136b5..b74d90c569 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -32,6 +32,7 @@ import ( "github.com/dominant-strategies/go-quai/core/state/snapshot" "github.com/dominant-strategies/go-quai/core/types" "github.com/dominant-strategies/go-quai/crypto" + "github.com/dominant-strategies/go-quai/ethdb" "github.com/dominant-strategies/go-quai/log" "github.com/dominant-strategies/go-quai/metrics_config" "github.com/dominant-strategies/go-quai/rlp" @@ -45,10 +46,10 @@ type revision struct { var ( // emptyRoot is the known root hash of an empty trie. - emptyRoot = common.HexToHash("56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421") - newestEtxKey = common.HexToHash("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffff") // max hash - oldestEtxKey = common.HexToHash("0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffe") // max hash - 1 - + emptyRoot = common.HexToHash("56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421") + newestEtxKey = common.HexToHash("0xffffffffffffffffffffffffffffffffffffffffffffffffffffffff") // max hash + oldestEtxKey = common.HexToHash("0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffe") // max hash - 1 + currentEpochKey = common.HexToHash("0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffd") // max hash - 2 ) type proofList [][]byte @@ -349,6 +350,7 @@ func (s *StateDB) TxIndex() int { } func (s *StateDB) GetCode(addr common.InternalAddress) []byte { + return []byte{} stateObject := s.getStateObject(addr) if stateObject != nil { return stateObject.Code(s.db) @@ -422,6 +424,10 @@ func (s *StateDB) ETXDatabase() Database { return s.etxDb } +func (s *StateDB) UnderlyingDatabase() ethdb.KeyValueReader { + return s.db.TrieDB().DiskDB() +} + // StorageTrie returns the storage trie of an account. // The return value is a copy and is nil for non-existent accounts. func (s *StateDB) StorageTrie(addr common.InternalAddress) Trie { @@ -702,6 +708,30 @@ func (s *StateDB) CommitEtxs() (common.Hash, error) { return root, err } +func (s *StateDB) GetLatestEpoch() (uint32, error) { + epochBytes, err := s.etxTrie.TryGet(currentEpochKey[:]) + if err != nil { + return 0, err + } + if epochBytes == nil || len(epochBytes) == 0 { + return 0, nil + } + if len(epochBytes) != 4 { + return 0, fmt.Errorf("invalid epoch length: %d", len(epochBytes)) + } + epoch := binary.BigEndian.Uint32(epochBytes) + return epoch, nil +} + +func (s *StateDB) SetLatestEpoch(epoch uint32) error { + epochBytes := make([]byte, 4) + binary.BigEndian.PutUint32(epochBytes, epoch) + if err := s.etxTrie.TryUpdate(currentEpochKey[:], epochBytes); err != nil { + return err + } + return nil +} + // getDeletedStateObject is similar to getStateObject, but instead of returning // nil for a deleted state object, it returns the actual object with the deleted // flag set. This is needed by the state journal to revert to the correct s- diff --git a/core/state_processor.go b/core/state_processor.go index 328c392556..11ab56d7b3 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -29,7 +29,6 @@ import ( "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2" lru "github.com/hashicorp/golang-lru/v2" - "github.com/holiman/uint256" "github.com/dominant-strategies/go-quai/common" "github.com/dominant-strategies/go-quai/common/prque" @@ -207,10 +206,13 @@ func NewStateProcessor(config *params.ChainConfig, hc *HeaderChain, engine conse } type UtxosCreatedDeleted struct { - UtxosCreatedKeys [][]byte - UtxosCreatedHashes []common.Hash - UtxosDeleted []*types.SpentUtxoEntry - UtxosDeletedHashes []common.Hash + UtxosCreatedKeys [][]byte + UtxosCreatedHashes []common.Hash + UtxosDeleted []*types.SpentUtxoEntry + UtxosDeletedHashes []common.Hash + CoinbaseLockupsCreated map[string]common.Hash + CoinbaseLockupsDeleted map[string]common.Hash + RotatedEpochs map[string]struct{} } // Process processes the state changes according to the Quai rules by running @@ -241,6 +243,9 @@ func (p *StateProcessor) Process(block *types.WorkObject, batch ethdb.Batch) (ty } time1 := common.PrettyDuration(time.Since(start)) + // enable the batch pending cache + batch.SetPending(true) + parentEvmRoot := parent.Header().EVMRoot() parentEtxSetRoot := parent.Header().EtxSetRoot() parentQuaiStateSize := parent.QuaiStateSize() @@ -258,6 +263,9 @@ func (p *StateProcessor) Process(block *types.WorkObject, batch ethdb.Batch) (ty return types.Receipts{}, []*types.Transaction{}, []*types.Log{}, nil, 0, 0, 0, nil, nil, err } utxosCreatedDeleted := new(UtxosCreatedDeleted) // utxos created and deleted in this block + utxosCreatedDeleted.CoinbaseLockupsCreated = make(map[string]common.Hash) + utxosCreatedDeleted.CoinbaseLockupsDeleted = make(map[string]common.Hash) + utxosCreatedDeleted.RotatedEpochs = make(map[string]struct{}) // Apply the previous inbound ETXs to the ETX set state prevInboundEtxs := rawdb.ReadInboundEtxs(p.hc.bc.db, header.ParentHash(nodeCtx)) if len(prevInboundEtxs) > 0 { @@ -267,6 +275,15 @@ func (p *StateProcessor) Process(block *types.WorkObject, batch ethdb.Batch) (ty } time2 := common.PrettyDuration(time.Since(start)) + coinbaseLockupEpoch, err := statedb.GetLatestEpoch() + if err != nil { + return nil, nil, nil, nil, 0, 0, 0, nil, nil, fmt.Errorf("could not get latest epoch: %w", err) + } + if blockNumber.Uint64()%params.CoinbaseEpochBlocks == 0 || coinbaseLockupEpoch == 0 { + coinbaseLockupEpoch++ + statedb.SetLatestEpoch(coinbaseLockupEpoch) + } + var timeSign, timePrepare, timeQiToQuai, timeQuaiToQi, timeCoinbase, timeEtx, timeTx time.Duration startTimeSenders := time.Now() senders := make(map[common.Hash]*common.InternalAddress) // temporary cache for senders of internal txs @@ -294,7 +311,7 @@ func (p *StateProcessor) Process(block *types.WorkObject, batch ethdb.Batch) (ty if err != nil { return nil, nil, nil, nil, 0, 0, 0, nil, nil, err } - vmenv := vm.NewEVM(blockContext, vm.TxContext{}, statedb, p.config, p.vmConfig) + vmenv := vm.NewEVM(blockContext, vm.TxContext{}, statedb, p.config, p.vmConfig, batch) time3 := common.PrettyDuration(time.Since(start)) // Iterate over and process the individual transactions. @@ -435,6 +452,63 @@ func (p *StateProcessor) Process(block *types.WorkObject, batch ethdb.Batch) (ty return nil, nil, nil, nil, 0, 0, 0, nil, nil, fmt.Errorf("invalid external transaction: etx %x is not in order or not found in unspent etx set", tx.Hash()) } + if etx.EtxType() == types.CoinbaseLockupType { + // This is either an unlocked Qi coinbase that was redeemed or Wrapped Qi + // An unlocked/redeemed Quai coinbase ETX is processed below as a standard Quai ETX + if tx.To().IsInQiLedgerScope() { + txGas := tx.Gas() + denominations := misc.FindMinDenominations(etx.Value()) + total := big.NewInt(0) + outputIndex := uint16(0) + success := true + // Iterate over the denominations in descending order + for denomination := types.MaxDenomination; denomination >= 0; denomination-- { + // If the denomination count is zero, skip it + if denominations[uint8(denomination)] == 0 { + continue + } + + for j := uint64(0); j < denominations[uint8(denomination)]; j++ { + if txGas < params.CallValueTransferGas || outputIndex >= types.MaxOutputIndex { + // No more gas, the rest of the denominations are lost but the tx is still valid + success = false + break + } + txGas -= params.CallValueTransferGas + if err := gp.SubGas(params.CallValueTransferGas); err != nil { + return nil, nil, nil, nil, 0, 0, 0, nil, nil, err + } + *usedGas += params.CallValueTransferGas // In the future we may want to determine what a fair gas cost is + totalEtxGas += params.CallValueTransferGas // In the future we may want to determine what a fair gas cost is + utxo := types.NewUtxoEntry(types.NewTxOut(uint8(denomination), etx.To().Bytes(), big.NewInt(0))) + // the ETX hash is guaranteed to be unique + if err := rawdb.CreateUTXO(batch, etx.Hash(), outputIndex, utxo); err != nil { + return nil, nil, nil, nil, 0, 0, 0, nil, nil, err + } + utxosCreatedDeleted.UtxosCreatedHashes = append(utxosCreatedDeleted.UtxosCreatedHashes, types.UTXOHash(etx.Hash(), outputIndex, utxo)) + utxosCreatedDeleted.UtxosCreatedKeys = append(utxosCreatedDeleted.UtxosCreatedKeys, rawdb.UtxoKeyWithDenomination(etx.Hash(), outputIndex, utxo.Denomination)) + p.logger.Debugf("Converting Quai to Qi %032x with denomination %d index %d lock %d\n", tx.Hash(), denomination, outputIndex, 0) + total.Add(total, types.Denominations[uint8(denomination)]) + outputIndex++ + } + } + receipt := &types.Receipt{Type: tx.Type(), Status: types.ReceiptStatusSuccessful, GasUsed: etx.Gas() - txGas, TxHash: tx.Hash(), + Logs: []*types.Log{{ + Address: *etx.To(), + Topics: []common.Hash{types.QuaiToQiConversionTopic}, + Data: total.Bytes(), + }}, + } + + if !success { + receipt.Status = types.ReceiptStatusFailed + receipt.GasUsed = etx.Gas() + } + receipts = append(receipts, receipt) + allLogs = append(allLogs, receipt.Logs...) + } + } + // check if the tx is a coinbase tx // coinbase tx // 1) is a external tx type @@ -452,107 +526,148 @@ func (p *StateProcessor) Process(block *types.WorkObject, batch ethdb.Batch) (ty return nil, nil, nil, nil, 0, 0, 0, nil, nil, fmt.Errorf("coinbase tx %x has invalid recipient: %w", tx.Hash(), err) } lockupByte := tx.Data()[0] + if int(lockupByte) > len(params.LockupByteToBlockDepth)-1 { + return nil, nil, nil, nil, 0, 0, 0, nil, nil, fmt.Errorf("coinbase lockup byte %d is out of range", lockupByte) + } if tx.To().IsInQiLedgerScope() { // Qi coinbase - if int(lockupByte) > len(params.LockupByteToBlockDepth)-1 { - return nil, nil, nil, nil, 0, 0, 0, nil, nil, fmt.Errorf("coinbase lockup byte %d is out of range", lockupByte) + _, err := tx.To().InternalAndQiAddress() + if err != nil { + return nil, nil, nil, nil, 0, 0, 0, nil, nil, fmt.Errorf("coinbase tx %x has invalid recipient: %w", tx.Hash(), err) } - var lockup *big.Int - // The first lock up period changes after the fork - if lockupByte == 0 { - if block.NumberU64(common.ZONE_CTX) < params.GoldenAgeForkNumberV1 { - lockup = new(big.Int).SetUint64(params.OldConversionLockPeriod) - } else { - lockup = new(big.Int).SetUint64(params.NewConversionLockPeriod) - } - } else { - lockup = new(big.Int).SetUint64(params.LockupByteToBlockDepth[lockupByte]) - if lockup.Uint64() < params.OldConversionLockPeriod { - return nil, nil, nil, nil, 0, 0, 0, nil, nil, fmt.Errorf("coinbase lockup period is less than the minimum lockup period of %d blocks", params.OldConversionLockPeriod) - } + total := big.NewInt(0) + lockup := new(big.Int).SetUint64(params.LockupByteToBlockDepth[lockupByte]) + if lockup.Uint64() < params.OldConversionLockPeriod { + return nil, nil, nil, nil, 0, 0, 0, nil, nil, fmt.Errorf("coinbase lockup period is less than the minimum lockup period of %d blocks", params.OldConversionLockPeriod) } lockup.Add(lockup, blockNumber) value := params.CalculateCoinbaseValueWithLockup(tx.Value(), lockupByte) - denominations := misc.FindMinDenominations(value) - outputIndex := uint16(0) - // Iterate over the denominations in descending order - for denomination := types.MaxDenomination; denomination >= 0; denomination-- { - // If the denomination count is zero, skip it - if denominations[uint8(denomination)] == 0 { - continue + if len(tx.Data()) == 1 { + denominations := misc.FindMinDenominations(value) + outputIndex := uint16(0) + // Iterate over the denominations in descending order + for denomination := types.MaxDenomination; denomination >= 0; denomination-- { + // If the denomination count is zero, skip it + if denominations[uint8(denomination)] == 0 { + continue + } + for j := uint64(0); j < denominations[uint8(denomination)]; j++ { + if outputIndex >= types.MaxOutputIndex { + // No more gas, the rest of the denominations are lost but the tx is still valid + break + } + utxo := types.NewUtxoEntry(types.NewTxOut(uint8(denomination), tx.To().Bytes(), lockup)) + // the ETX hash is guaranteed to be unique + if err := rawdb.CreateUTXO(batch, etx.Hash(), outputIndex, utxo); err != nil { + return nil, nil, nil, nil, 0, 0, 0, nil, nil, err + } + utxosCreatedDeleted.UtxosCreatedHashes = append(utxosCreatedDeleted.UtxosCreatedHashes, types.UTXOHash(etx.Hash(), outputIndex, utxo)) + utxosCreatedDeleted.UtxosCreatedKeys = append(utxosCreatedDeleted.UtxosCreatedKeys, rawdb.UtxoKeyWithDenomination(etx.Hash(), outputIndex, utxo.Denomination)) + p.logger.Debugf("Creating UTXO for coinbase %032x with denomination %d index %d\n", tx.Hash(), denomination, outputIndex) + total.Add(total, types.Denominations[uint8(denomination)]) + outputIndex++ + } } - for j := uint64(0); j < denominations[uint8(denomination)]; j++ { - if outputIndex >= types.MaxOutputIndex { - // No more gas, the rest of the denominations are lost but the tx is still valid - break + receipt = &types.Receipt{Type: tx.Type(), Status: types.ReceiptStatusSuccessful, GasUsed: 0, TxHash: tx.Hash()} + } else if len(tx.Data()) == common.AddressLength+1 { + contractAddr := common.BytesToAddress(tx.Data()[1:], nodeLocation) + internal, err := tx.To().InternalAndQiAddress() + if err != nil { + return nil, nil, nil, nil, 0, 0, 0, nil, nil, fmt.Errorf("coinbase tx %x has invalid recipient: %w", tx.Hash(), err) + } + if statedb.GetCode(internal) == nil { + // No code at contract address + // Coinbase reward is lost + // Justification: We should not store a coinbase lockup that can never be claimed + receipt = &types.Receipt{Type: tx.Type(), Status: types.ReceiptStatusFailed, GasUsed: 0, TxHash: tx.Hash()} + } else { + + oldCoinbaseLockupKey, newCoinbaseLockupKey, oldCoinbaseLockupHash, newCoinbaseLockupHash, err := vm.AddNewLock(statedb, batch, contractAddr, *etx.To(), common.OneInternal(nodeLocation), lockupByte, lockup.Uint64(), coinbaseLockupEpoch, value, nodeLocation, true) + if err != nil || newCoinbaseLockupHash == nil { + return nil, nil, nil, nil, 0, 0, 0, nil, nil, fmt.Errorf("could not add new lock: %w", err) } - utxo := types.NewUtxoEntry(types.NewTxOut(uint8(denomination), tx.To().Bytes(), lockup)) - // the ETX hash is guaranteed to be unique - if err := rawdb.CreateUTXO(batch, etx.Hash(), outputIndex, utxo); err != nil { - return nil, nil, nil, nil, 0, 0, 0, nil, nil, err + // Store the new lockup key every time + utxosCreatedDeleted.CoinbaseLockupsCreated[string(newCoinbaseLockupKey)] = *newCoinbaseLockupHash + + if oldCoinbaseLockupHash != nil { + // We deleted (updated) the old lockup, write it to deleted list but only the first time + if _, exists := utxosCreatedDeleted.CoinbaseLockupsDeleted[string(oldCoinbaseLockupKey)]; !exists { + if _, exists := utxosCreatedDeleted.RotatedEpochs[string(newCoinbaseLockupKey)]; !exists { + // We only want to add a delete if we have not rotated the epoch (we haven't created a new lock) because otherwise there is nothing to delete + utxosCreatedDeleted.CoinbaseLockupsDeleted[string(oldCoinbaseLockupKey)] = *oldCoinbaseLockupHash + utxosCreatedDeleted.UtxosDeletedHashes = append(utxosCreatedDeleted.UtxosDeletedHashes, *oldCoinbaseLockupHash) + } + } + } else { + // If we did not delete, we are rotating the epoch and need to store it + utxosCreatedDeleted.RotatedEpochs[string(newCoinbaseLockupKey)] = struct{}{} } - utxosCreatedDeleted.UtxosCreatedHashes = append(utxosCreatedDeleted.UtxosCreatedHashes, types.UTXOHash(etx.Hash(), outputIndex, utxo)) - utxosCreatedDeleted.UtxosCreatedKeys = append(utxosCreatedDeleted.UtxosCreatedKeys, rawdb.UtxoKeyWithDenomination(etx.Hash(), outputIndex, utxo.Denomination)) - p.logger.Debugf("Creating UTXO for coinbase %032x with denomination %d index %d\n", tx.Hash(), denomination, outputIndex) - outputIndex++ + receipt = &types.Receipt{Type: tx.Type(), Status: types.ReceiptStatusSuccessful, GasUsed: 0, TxHash: tx.Hash()} // todo: consider adding the reward to the receipt in a log } + } else { + // Coinbase data is either too long or too small + // Coinbase reward is lost + receipt = &types.Receipt{Type: tx.Type(), Status: types.ReceiptStatusFailed, GasUsed: 0, TxHash: tx.Hash()} } + receipts = append(receipts, receipt) + allLogs = append(allLogs, receipt.Logs...) } else if tx.To().IsInQuaiLedgerScope() { // Quai coinbase - internal, err := tx.To().InternalAndQuaiAddress() + _, err := tx.To().InternalAndQuaiAddress() if err != nil { return nil, nil, nil, nil, 0, 0, 0, nil, nil, fmt.Errorf("coinbase tx %x has invalid recipient: %w", tx.Hash(), err) } - code := statedb.GetCode(internal) - if len(code) == 0 && len(tx.Data()) == 1 { - // No code and coinbase has no extra data + if len(tx.Data()) == 1 { // Coinbase is valid, no gas used receipt = &types.Receipt{Type: tx.Type(), Status: types.ReceiptStatusSuccessful, GasUsed: 0, TxHash: tx.Hash()} - } else if (len(code) == 0 && len(tx.Data()) > 1) || (len(code) > 0 && len(tx.Data()) < 5) { - // No code, but coinbase has extra data - // Or there is code, but coinbase data doesn't include 4-byte function sig + } else if len(tx.Data()) != common.AddressLength+1 { + // Coinbase data is either too long or too small // Coinbase reward is lost receipt = &types.Receipt{Type: tx.Type(), Status: types.ReceiptStatusFailed, GasUsed: 0, TxHash: tx.Hash()} - } else { + } else { // Quai coinbase lockup contract // Create params for uint256 lockup, uint256 balance, address recipient lockup := new(big.Int).SetUint64(params.LockupByteToBlockDepth[lockupByte]) if lockup.Uint64() < params.OldConversionLockPeriod { return nil, nil, nil, nil, 0, 0, 0, nil, nil, fmt.Errorf("coinbase lockup period is less than the minimum lockup period of %d blocks", params.OldConversionLockPeriod) } lockup.Add(lockup, blockNumber) - sig := tx.Data()[1:5] - data := make([]byte, 0, 0) - data = append(data, sig...) - if len(tx.Data()) > 5 { - // If there is extra data, append it to the data (for example, reward beneficiary) - data = append(data, tx.Data()[5:]...) - } - reward, overflow := uint256.FromBig(params.CalculateCoinbaseValueWithLockup(tx.Value(), lockupByte)) - if overflow { - return nil, nil, nil, nil, 0, 0, 0, nil, nil, fmt.Errorf("coinbase value overflow") - } - temp := reward.Bytes32() - data = append(data, temp[:]...) - lockup256, overflow := uint256.FromBig(lockup) - if overflow { - return nil, nil, nil, nil, 0, 0, 0, nil, nil, fmt.Errorf("coinbase lockup overflow") - } - temp = lockup256.Bytes32() - data = append(data, temp[:]...) - msg.SetData(data) - msg.SetValue(big.NewInt(0)) - - gp := new(types.GasPool).AddGas(params.CoinbaseGas) - prevZeroBal := prepareApplyCoinbaseLockup(statedb, nodeLocation) - // This lockup contract transaction comes from the 0x000...1 address, so the contract should only accept calls from this address - receipt, _, err = applyTransaction(msg, parent, p.config, p.hc, gp, statedb, blockNumber, blockHash, etx, usedGas, usedState, vmenv, new(int), new(int), p.logger) - statedb.SetBalance(common.OneInternal(nodeLocation), prevZeroBal) // Reset the balance to what it previously was. Residual balance will be lost + + contractAddr := common.BytesToAddress(tx.Data()[1:], nodeLocation) + internal, err := contractAddr.InternalAndQuaiAddress() if err != nil { - return nil, nil, nil, nil, 0, 0, 0, nil, nil, fmt.Errorf("could not apply tx %d [%v]: %w", i, tx.Hash().Hex(), err) + return nil, nil, nil, nil, 0, 0, 0, nil, nil, fmt.Errorf("coinbase tx %x has invalid recipient: %w", tx.Hash(), err) + } + if statedb.GetCode(internal) == nil { + // No code at contract address + // Coinbase reward is lost + // Justification: We should not store a coinbase lockup that can never be claimed + receipt = &types.Receipt{Type: tx.Type(), Status: types.ReceiptStatusFailed, GasUsed: 0, TxHash: tx.Hash()} + } else { + reward := params.CalculateCoinbaseValueWithLockup(tx.Value(), lockupByte) + // Add the lockup owned by the smart contract with the miner as beneficiary + oldCoinbaseLockupKey, newCoinbaseLockupKey, oldCoinbaseLockupHash, newCoinbaseLockupHash, err := vm.AddNewLock(statedb, batch, contractAddr, *etx.To(), common.OneInternal(nodeLocation), lockupByte, lockup.Uint64(), coinbaseLockupEpoch, reward, nodeLocation, true) + if err != nil || newCoinbaseLockupHash == nil { + return nil, nil, nil, nil, 0, 0, 0, nil, nil, fmt.Errorf("could not add new lock: %w", err) + } + // Store the new lockup key every time + utxosCreatedDeleted.CoinbaseLockupsCreated[string(newCoinbaseLockupKey)] = *newCoinbaseLockupHash + + if oldCoinbaseLockupHash != nil { + // We deleted (updated) the old lockup, write it to deleted list but only the first time + if _, exists := utxosCreatedDeleted.CoinbaseLockupsDeleted[string(oldCoinbaseLockupKey)]; !exists { + if _, exists := utxosCreatedDeleted.RotatedEpochs[string(newCoinbaseLockupKey)]; !exists { + // Don't register deletes for any rotated epochs + utxosCreatedDeleted.CoinbaseLockupsDeleted[string(oldCoinbaseLockupKey)] = *oldCoinbaseLockupHash + utxosCreatedDeleted.UtxosDeletedHashes = append(utxosCreatedDeleted.UtxosDeletedHashes, *oldCoinbaseLockupHash) + } + } + } else { + // If we did not delete, we are rotating the epoch and need to store it + utxosCreatedDeleted.RotatedEpochs[string(newCoinbaseLockupKey)] = struct{}{} + } + receipt = &types.Receipt{Type: tx.Type(), Status: types.ReceiptStatusSuccessful, GasUsed: 0, TxHash: tx.Hash()} } - totalEtxGas += receipt.GasUsed } receipts = append(receipts, receipt) allLogs = append(allLogs, receipt.Logs...) - i++ } if block.NumberU64(common.ZONE_CTX) > params.TimeToStartTx { // subtract the minimum tx gas from the gas pool @@ -620,22 +735,16 @@ func (p *StateProcessor) Process(block *types.WorkObject, batch ethdb.Batch) (ty outputIndex++ } } - if success { - receipt = &types.Receipt{Type: tx.Type(), Status: types.ReceiptStatusSuccessful, GasUsed: etx.Gas() - txGas, TxHash: tx.Hash(), - Logs: []*types.Log{{ - Address: *etx.To(), - Topics: []common.Hash{types.QuaiToQiConversionTopic}, - Data: total.Bytes(), - }}, - } - } else { - receipt = &types.Receipt{Type: tx.Type(), Status: types.ReceiptStatusFailed, GasUsed: etx.Gas(), TxHash: tx.Hash(), - Logs: []*types.Log{{ - Address: *etx.To(), - Topics: []common.Hash{types.QuaiToQiConversionTopic}, - Data: total.Bytes(), - }}, - } + receipt := &types.Receipt{Type: tx.Type(), Status: types.ReceiptStatusSuccessful, GasUsed: etx.Gas() - txGas, TxHash: tx.Hash(), + Logs: []*types.Log{{ + Address: *etx.To(), + Topics: []common.Hash{types.QuaiToQiConversionTopic}, + Data: total.Bytes(), + }}, + } + if !success { + receipt.Status = types.ReceiptStatusFailed + receipt.GasUsed = etx.Gas() } receipts = append(receipts, receipt) allLogs = append(allLogs, receipt.Logs...) @@ -668,6 +777,7 @@ func (p *StateProcessor) Process(block *types.WorkObject, batch ethdb.Batch) (ty continue // locked and redeemed later } // Apply ETX to Quai state + // This could also be an unlocked Quai coinbase redemption ETX, the process is the same fees := big.NewInt(0) prevZeroBal := prepareApplyETX(statedb, msg.Value(), nodeLocation) receipt, fees, err = applyTransaction(msg, parent, p.config, p.hc, gp, statedb, blockNumber, blockHash, etx, usedGas, usedState, vmenv, &etxRLimit, &etxPLimit, p.logger) @@ -785,19 +895,19 @@ func (p *StateProcessor) Process(block *types.WorkObject, batch ethdb.Batch) (ty coinbaseReward := misc.CalculateReward(parent, block.WorkObjectHeader()) blockReward := new(big.Int).Add(coinbaseReward, quaiFees) - coinbaseEtx := types.NewTx(&types.ExternalTx{To: &primaryCoinbase, Gas: params.TxGas, Value: blockReward, EtxType: types.CoinbaseType, OriginatingTxHash: common.SetBlockHashForQuai(parentHash, nodeLocation), ETXIndex: uint16(len(emittedEtxs)), Sender: primaryCoinbase, Data: []byte{block.Lock()}}) + coinbaseEtx := types.NewTx(&types.ExternalTx{To: &primaryCoinbase, Gas: params.TxGas, Value: blockReward, EtxType: types.CoinbaseType, OriginatingTxHash: common.SetBlockHashForQuai(parentHash, nodeLocation), ETXIndex: uint16(len(emittedEtxs)), Sender: primaryCoinbase, Data: block.Data()}) emittedEtxs = append(emittedEtxs, coinbaseEtx) if qiFees.Cmp(big.NewInt(0)) != 0 { - coinbaseEtx := types.NewTx(&types.ExternalTx{To: &secondaryCoinbase, Gas: params.TxGas, Value: qiFees, EtxType: types.CoinbaseType, OriginatingTxHash: common.SetBlockHashForQi(parentHash, nodeLocation), ETXIndex: uint16(len(emittedEtxs)), Sender: secondaryCoinbase, Data: []byte{block.Lock()}}) + coinbaseEtx := types.NewTx(&types.ExternalTx{To: &secondaryCoinbase, Gas: params.TxGas, Value: qiFees, EtxType: types.CoinbaseType, OriginatingTxHash: common.SetBlockHashForQi(parentHash, nodeLocation), ETXIndex: uint16(len(emittedEtxs)), Sender: secondaryCoinbase, Data: block.Data()}) emittedEtxs = append(emittedEtxs, coinbaseEtx) } } else if bytes.Equal(block.PrimaryCoinbase().Bytes(), qiCoinbase.Bytes()) { coinbaseReward := misc.CalculateReward(parent, block.WorkObjectHeader()) blockReward := new(big.Int).Add(coinbaseReward, qiFees) - coinbaseEtx := types.NewTx(&types.ExternalTx{To: &primaryCoinbase, Gas: params.TxGas, Value: blockReward, EtxType: types.CoinbaseType, OriginatingTxHash: common.SetBlockHashForQi(parentHash, nodeLocation), ETXIndex: uint16(len(emittedEtxs)), Sender: primaryCoinbase, Data: []byte{block.Lock()}}) + coinbaseEtx := types.NewTx(&types.ExternalTx{To: &primaryCoinbase, Gas: params.TxGas, Value: blockReward, EtxType: types.CoinbaseType, OriginatingTxHash: common.SetBlockHashForQi(parentHash, nodeLocation), ETXIndex: uint16(len(emittedEtxs)), Sender: primaryCoinbase, Data: block.Data()}) emittedEtxs = append(emittedEtxs, coinbaseEtx) if quaiFees.Cmp(big.NewInt(0)) != 0 { - coinbaseEtx := types.NewTx(&types.ExternalTx{To: &secondaryCoinbase, Gas: params.TxGas, Value: quaiFees, EtxType: types.CoinbaseType, OriginatingTxHash: common.SetBlockHashForQuai(parentHash, nodeLocation), ETXIndex: uint16(len(emittedEtxs)), Sender: secondaryCoinbase, Data: []byte{block.Lock()}}) + coinbaseEtx := types.NewTx(&types.ExternalTx{To: &secondaryCoinbase, Gas: params.TxGas, Value: quaiFees, EtxType: types.CoinbaseType, OriginatingTxHash: common.SetBlockHashForQuai(parentHash, nodeLocation), ETXIndex: uint16(len(emittedEtxs)), Sender: secondaryCoinbase, Data: block.Data()}) emittedEtxs = append(emittedEtxs, coinbaseEtx) } } @@ -811,7 +921,7 @@ func (p *StateProcessor) Process(block *types.WorkObject, batch ethdb.Batch) (ty } else { originHash = common.SetBlockHashForQi(parentHash, nodeLocation) } - emittedEtxs = append(emittedEtxs, types.NewTx(&types.ExternalTx{To: &uncleCoinbase, Gas: params.TxGas, Value: reward, EtxType: types.CoinbaseType, OriginatingTxHash: originHash, ETXIndex: uint16(len(emittedEtxs)), Sender: uncleCoinbase, Data: []byte{uncle.Lock()}})) + emittedEtxs = append(emittedEtxs, types.NewTx(&types.ExternalTx{To: &uncleCoinbase, Gas: params.TxGas, Value: reward, EtxType: types.CoinbaseType, OriginatingTxHash: originHash, ETXIndex: uint16(len(emittedEtxs)), Sender: uncleCoinbase, Data: uncle.Data()})) } updatedTokenChoiceSet, err := CalculateTokenChoicesSet(p.hc, parent, emittedEtxs) @@ -858,6 +968,11 @@ func (p *StateProcessor) Process(block *types.WorkObject, batch ethdb.Batch) (ty } } + for _, hash := range utxosCreatedDeleted.CoinbaseLockupsCreated { + // Update the created hash list with the latest new elements (instead of intermediate ones) + utxosCreatedDeleted.UtxosCreatedHashes = append(utxosCreatedDeleted.UtxosCreatedHashes, hash) + } + time4 := common.PrettyDuration(time.Since(start)) // Finalize the block, applying any consensus engine specific extras (e.g. block rewards) multiSet, utxoSetSize, err := p.engine.Finalize(p.hc, batch, block, statedb, false, parentUtxoSetSize, utxosCreatedDeleted.UtxosCreatedHashes, utxosCreatedDeleted.UtxosDeletedHashes) @@ -971,23 +1086,11 @@ func RedeemLockedQuai(hc *HeaderChain, header *types.WorkObject, parent *types.W if targetBlock == nil { return nil, fmt.Errorf("block at height %d not found", targetBlockHeight) } - receipts := hc.bc.processor.GetReceiptsByHash(targetBlock.Hash()) + for _, etx := range targetBlock.Body().ExternalTransactions() { // Check if the transaction is a coinbase transaction if types.IsCoinBaseTx(etx) && etx.To().IsInQuaiLedgerScope() { - var txReceipt *types.Receipt - for _, receipt := range receipts { - if receipt.TxHash == etx.Hash() { - txReceipt = receipt - } - } - if txReceipt == nil { - return nil, fmt.Errorf("coinbase receipt not found for %s", etx.Hash().Hex()) - } - if txReceipt.Status == types.ReceiptStatusFailed { - // The coinbase reward is lost - continue - } + if len(etx.Data()) == 1 { // Redeem all unlocked Quai for the coinbase address internal, err := etx.To().InternalAddress() @@ -1019,45 +1122,9 @@ func RedeemLockedQuai(hc *HeaderChain, header *types.WorkObject, parent *types.W Amt: balance, }) } - } else if len(etx.Data()) >= 5 { - lockupByte := etx.Data()[0] - reward := params.CalculateCoinbaseValueWithLockup(etx.Value(), lockupByte) - if params.LockupByteToBlockDepth[lockupByte] == blockDepth { - msg, err := etx.AsMessage(types.MakeSigner(hc.config, header.Number(common.ZONE_CTX)), header.BaseFee()) - if err != nil { - return nil, fmt.Errorf("could not apply tx %d [%v]: %w", i, etx.Hash().Hex(), err) - } - sig := etx.Data()[1:5] - data := make([]byte, 0, 0) - data = append(data, sig...) - if len(etx.Data()) > 5 { - // If there is extra data, append it to the data (for example, reward beneficiary) - data = append(data, etx.Data()[5:]...) - } - reward256, overflow := uint256.FromBig(reward) - if overflow { - return nil, fmt.Errorf("coinbase value overflow") - } - temp := reward256.Bytes32() - data = append(data, temp[:]...) - lockup256, overflow := uint256.FromBig(new(big.Int).SetUint64(blockDepth)) - if overflow { - return nil, fmt.Errorf("coinbase lockup overflow") - } - temp = lockup256.Bytes32() - data = append(data, temp[:]...) - - msg.SetData(data) - msg.SetValue(reward) - - gp := new(types.GasPool).AddGas(params.CoinbaseGas) - prevZeroBal := prepareApplyETX(statedb, msg.Value(), hc.NodeLocation()) - _, _, err = applyTransaction(msg, parent, hc.config, hc, gp, statedb, header.Number(common.ZONE_CTX), header.Hash(), etx, new(uint64), new(uint64), vmenv, new(int), new(int), hc.logger) - statedb.SetBalance(common.ZeroInternal(hc.NodeLocation()), prevZeroBal) // Reset the balance to what it previously was. Residual balance will be lost - if err != nil { - return nil, fmt.Errorf("could not apply tx %d [%v]: %w", i, etx.Hash().Hex(), err) - } - } + } else if len(etx.Data()) == common.AddressLength+1 { + // This coinbase is owned by a smart contract and must be unlocked manually + continue } } @@ -1140,9 +1207,10 @@ func applyTransaction(msg types.Message, parent *types.WorkObject, config *param // Create a new receipt for the transaction, storing the intermediate root and gas used // by the tx. - receipt := &types.Receipt{Type: tx.Type(), PostState: root, CumulativeGasUsed: *usedGas, OutboundEtxs: result.Etxs} + receipt := &types.Receipt{Type: tx.Type(), PostState: root, CumulativeGasUsed: *usedGas} if result.Failed() { receipt.Status = types.ReceiptStatusFailed + evm.UndoCoinbasesDeleted() logger.WithField("err", result.Err).Debug("Transaction failed") } else { receipt.Status = types.ReceiptStatusSuccessful @@ -1150,6 +1218,7 @@ func applyTransaction(msg types.Message, parent *types.WorkObject, config *param if result.ContractAddr != nil { receipt.ContractAddress = *result.ContractAddr } + receipt.OutboundEtxs = result.Etxs } receipt.TxHash = tx.Hash() receipt.GasUsed = result.UsedGas @@ -1773,7 +1842,7 @@ func (p *StateProcessor) Apply(batch ethdb.Batch, block *types.WorkObject) ([]*t // and uses the input parameters for its environment. It returns the receipt // for the transaction, gas used and an error if the transaction failed, // indicating the block was invalid. -func ApplyTransaction(config *params.ChainConfig, parent *types.WorkObject, parentOrder int, bc ChainContext, author *common.Address, gp *types.GasPool, statedb *state.StateDB, header *types.WorkObject, tx *types.Transaction, usedGas *uint64, usedState *uint64, cfg vm.Config, etxRLimit, etxPLimit *int, logger *log.Logger) (*types.Receipt, *big.Int, error) { +func ApplyTransaction(config *params.ChainConfig, parent *types.WorkObject, parentOrder int, bc ChainContext, author *common.Address, gp *types.GasPool, statedb *state.StateDB, header *types.WorkObject, tx *types.Transaction, usedGas *uint64, usedState *uint64, cfg vm.Config, etxRLimit, etxPLimit *int, batch ethdb.Batch, logger *log.Logger) (*types.Receipt, *big.Int, error) { nodeCtx := config.Location.Context() msg, err := tx.AsMessage(types.MakeSigner(config, header.Number(nodeCtx)), header.BaseFee()) if err != nil { @@ -1784,7 +1853,7 @@ func ApplyTransaction(config *params.ChainConfig, parent *types.WorkObject, pare if err != nil { return nil, nil, err } - vmenv := vm.NewEVM(blockContext, vm.TxContext{}, statedb, config, cfg) + vmenv := vm.NewEVM(blockContext, vm.TxContext{}, statedb, config, cfg, batch) if tx.Type() == types.ExternalTxType { prevZeroBal := prepareApplyETX(statedb, msg.Value(), config.Location) receipt, quaiFees, err := applyTransaction(msg, parent, config, bc, gp, statedb, header.Number(nodeCtx), header.Hash(), tx, usedGas, usedState, vmenv, etxRLimit, etxPLimit, logger) @@ -1794,26 +1863,6 @@ func ApplyTransaction(config *params.ChainConfig, parent *types.WorkObject, pare return applyTransaction(msg, parent, config, bc, gp, statedb, header.Number(nodeCtx), header.Hash(), tx, usedGas, usedState, vmenv, etxRLimit, etxPLimit, logger) } -func ApplyCoinbaseLockupTransaction(config *params.ChainConfig, parent *types.WorkObject, parentOrder int, bc ChainContext, author *common.Address, gp *types.GasPool, statedb *state.StateDB, header *types.WorkObject, tx *types.Transaction, usedGas *uint64, usedState *uint64, cfg vm.Config, etxRLimit, etxPLimit *int, data []byte, logger *log.Logger) (*types.Receipt, *big.Int, error) { - nodeCtx := config.Location.Context() - msg, err := tx.AsMessage(types.MakeSigner(config, header.Number(nodeCtx)), header.BaseFee()) - if err != nil { - return nil, nil, err - } - // Create a new context to be used in the EVM environment - blockContext, err := NewEVMBlockContext(header, parent, bc, author) - if err != nil { - return nil, nil, err - } - msg.SetData(data) - msg.SetValue(big.NewInt(0)) - vmenv := vm.NewEVM(blockContext, vm.TxContext{}, statedb, config, cfg) - prevZeroBal := prepareApplyCoinbaseLockup(statedb, config.Location) - receipt, quaiFees, err := applyTransaction(msg, parent, config, bc, gp, statedb, header.Number(nodeCtx), header.Hash(), tx, usedGas, usedState, vmenv, etxRLimit, etxPLimit, logger) - statedb.SetBalance(common.OneInternal(config.Location), prevZeroBal) // Reset the balance to what it previously was (currently a failed external transaction removes all the sent coins from the supply and any residual balance is gone as well) - return receipt, quaiFees, err -} - // GetVMConfig returns the block chain VM config. func (p *StateProcessor) GetVMConfig() *vm.Config { return &p.vmConfig @@ -2085,7 +2134,7 @@ func (p *StateProcessor) StateAtTransaction(block *types.WorkObject, txIndex int return msg, context, statedb, nil } // Not yet the searched for transaction, execute on top of the current state - vmenv := vm.NewEVM(context, txContext, statedb, p.hc.Config(), vm.Config{}) + vmenv := vm.NewEVM(context, txContext, statedb, p.hc.Config(), vm.Config{}, nil) statedb.Prepare(tx.Hash(), idx) if _, err := ApplyMessage(vmenv, msg, new(types.GasPool).AddGas(tx.Gas())); err != nil { return nil, vm.BlockContext{}, nil, fmt.Errorf("transaction %#x failed: %v", tx.Hash(), err) diff --git a/core/types/transaction.go b/core/types/transaction.go index 05c716c5af..c2d9f36648 100644 --- a/core/types/transaction.go +++ b/core/types/transaction.go @@ -54,6 +54,7 @@ const ( DefaultType = iota CoinbaseType ConversionType + CoinbaseLockupType ) const ( diff --git a/core/types/utxo.go b/core/types/utxo.go index c20d83a13c..38ccd79e7c 100644 --- a/core/types/utxo.go +++ b/core/types/utxo.go @@ -23,6 +23,8 @@ const ( var ( MaxQi = new(big.Int).Mul(big.NewInt(math.MaxInt64), big.NewInt(params.Ether)) // This is just a default; determine correct value later QuaiToQiConversionTopic = crypto.Keccak256Hash([]byte("QuaiToQiConversion")) + QuaiCoinbaseLockupTopic = crypto.Keccak256Hash([]byte("QuaiCoinbaseLockup")) + QiCoinbaseLockupTopic = crypto.Keccak256Hash([]byte("QiCoinbaseLockup")) ) // Denominations is a map of denomination to number of Qi @@ -474,3 +476,7 @@ func UTXOHash(txHash common.Hash, index uint16, utxo *UtxoEntry) common.Hash { binary.BigEndian.PutUint16(indexBytes, index) return RlpHash([]interface{}{txHash, indexBytes, utxo}) // TODO: Consider encoding to protobuf instead } + +func CoinbaseLockupHash(ownerContract common.Address, beneficiaryMiner common.Address, lockupByte byte, epoch uint32, balance *big.Int, unlockHeight uint32, elements uint16) common.Hash { + return RlpHash([]interface{}{ownerContract, beneficiaryMiner, lockupByte, epoch, balance, unlockHeight, elements}) // TODO: Consider encoding to protobuf instead +} diff --git a/core/vm/contracts.go b/core/vm/contracts.go index 8b2c2a7a17..c484279092 100644 --- a/core/vm/contracts.go +++ b/core/vm/contracts.go @@ -26,10 +26,15 @@ import ( "github.com/dominant-strategies/go-quai/common" "github.com/dominant-strategies/go-quai/common/math" + "github.com/dominant-strategies/go-quai/core/rawdb" + "github.com/dominant-strategies/go-quai/core/types" "github.com/dominant-strategies/go-quai/crypto" "github.com/dominant-strategies/go-quai/crypto/blake2b" "github.com/dominant-strategies/go-quai/crypto/bn256" + "github.com/dominant-strategies/go-quai/ethdb" + "github.com/dominant-strategies/go-quai/log" "github.com/dominant-strategies/go-quai/params" + "github.com/holiman/uint256" //lint:ignore SA1019 Needed for precompile "golang.org/x/crypto/ripemd160" @@ -44,8 +49,9 @@ type PrecompiledContract interface { } var ( - PrecompiledContracts map[common.AddressBytes]PrecompiledContract = make(map[common.AddressBytes]PrecompiledContract) - PrecompiledAddresses map[string][]common.Address = make(map[string][]common.Address) + PrecompiledContracts map[common.AddressBytes]PrecompiledContract = make(map[common.AddressBytes]PrecompiledContract) + PrecompiledAddresses map[string][]common.Address = make(map[string][]common.Address) + LockupContractAddresses map[[2]byte]common.Address = make(map[[2]byte]common.Address) // LockupContractAddress is not of type PrecompiledContract ) func InitializePrecompiles(nodeLocation common.Location) { @@ -58,6 +64,7 @@ func InitializePrecompiles(nodeLocation common.Location) { PrecompiledContracts[common.HexToAddressBytes(fmt.Sprintf("0x%02x00000000000000000000000000000000000007", nodeLocation.BytePrefix()))] = &bn256ScalarMul{} PrecompiledContracts[common.HexToAddressBytes(fmt.Sprintf("0x%02x00000000000000000000000000000000000008", nodeLocation.BytePrefix()))] = &bn256Pairing{} PrecompiledContracts[common.HexToAddressBytes(fmt.Sprintf("0x%02x00000000000000000000000000000000000009", nodeLocation.BytePrefix()))] = &blake2F{} + LockupContractAddresses[[2]byte{nodeLocation[0], nodeLocation[1]}] = common.HexToAddress(fmt.Sprintf("0x%02x0000000000000000000000000000000000000A", nodeLocation.BytePrefix()), nodeLocation) for address, _ := range PrecompiledContracts { if address.Location().Equal(nodeLocation) { @@ -67,7 +74,7 @@ func InitializePrecompiles(nodeLocation common.Location) { } } -// ActivePrecompiles returns the precompiles enabled with the current configuration. +// ActivePrecompiles returns the precompiles enabled with the current configuration, except the Lockup Contract. func ActivePrecompiles(rules params.Rules, nodeLocation common.Location) []common.Address { return PrecompiledAddresses[nodeLocation.Name()] } @@ -498,3 +505,226 @@ func intToByteArray20(n uint8) [20]byte { func RequiredGas(input []byte) uint64 { return 0 } + +func RunLockupContract(evm *EVM, ownerContract common.Address, gas *uint64, input []byte) ([]byte, error) { + switch len(input) { + case 33: + if err := ClaimCoinbaseLockup(evm, ownerContract, evm.Context.BlockNumber.Uint64(), gas, input); err != nil { + return nil, err + } else { + return []byte{1}, nil + } + case 25: + ret, err := GetLockupData(evm, ownerContract, input) + if err != nil { + return nil, err + } else { + return ret, nil + } + case 21: + ret, err := GetLatestLockupData(evm, ownerContract, input) + if err != nil { + return nil, err + } else { + return ret, nil + } + case 0: + epoch, err := evm.GetLatestMinerEpoch() + if err != nil { + return nil, err + } else { + epochBytes := make([]byte, 32) + binary.BigEndian.PutUint32(epochBytes[28:], epoch) // Right-align + return epochBytes, nil + } + default: + return nil, ErrExecutionReverted + } +} + +func GetLockupData(evm *EVM, ownerContract common.Address, input []byte) ([]byte, error) { + if len(input) != 25 { + return nil, errors.New("input length is not 25 bytes") + } + // Extract beneficiaryMiner + beneficiaryMiner := common.BytesToAddress(input[:20], evm.chainConfig.Location) + // Extract lockupByte + lockupByte := input[20] + + // Extract epoch + epoch := binary.BigEndian.Uint32(input[21:25]) + _, err := ownerContract.InternalAndQuaiAddress() + if err != nil { + return nil, err + } + _, err = beneficiaryMiner.InternalAddress() + if err != nil { + return nil, err + } + balance, trancheUnlockHeight, elements := rawdb.ReadCoinbaseLockup(evm.StateDB.UnderlyingDatabase(), evm.Batch, ownerContract, beneficiaryMiner, lockupByte, epoch) + + return ABIEncodeLockupData(trancheUnlockHeight, balance, elements) +} + +func GetLatestLockupData(evm *EVM, ownerContract common.Address, input []byte) ([]byte, error) { + if len(input) != 21 { + return nil, errors.New("input length is not 21 bytes") + } + // Extract beneficiaryMiner + beneficiaryMiner := common.BytesToAddress(input[:20], evm.chainConfig.Location) + // Extract lockupByte + lockupByte := input[20] + epoch, err := evm.GetLatestMinerEpoch() + if err != nil { + return nil, err + } + balance, trancheUnlockHeight, elements := rawdb.ReadCoinbaseLockup(evm.StateDB.UnderlyingDatabase(), evm.Batch, ownerContract, beneficiaryMiner, lockupByte, epoch) + return ABIEncodeLockupData(trancheUnlockHeight, balance, elements) +} + +func ABIEncodeLockupData(trancheUnlockHeight uint32, balance *big.Int, elements uint16) ([]byte, error) { + // Create a buffer for the result + encoded := make([]byte, 0, 96) // 32 bytes for each value + + // Encode trancheUnlockHeight (uint32, right-aligned to 32 bytes) + trancheBytes := make([]byte, 32) + binary.BigEndian.PutUint32(trancheBytes[28:], trancheUnlockHeight) // Right-align + encoded = append(encoded, trancheBytes...) + // Encode balance (32 bytes) + balanceBytes, overflow := uint256.FromBig(balance) + if overflow { + return nil, fmt.Errorf("balance is too large to encode: %v", balance) + } + temp := balanceBytes.Bytes32() + encoded = append(encoded, temp[:]...) + + // Encode elements (uint16, right-aligned to 32 bytes) + elementsBytes := make([]byte, 32) + binary.BigEndian.PutUint16(elementsBytes[30:], elements) // Right-align + encoded = append(encoded, elementsBytes...) + + return encoded, nil +} + +func ClaimCoinbaseLockup(evm *EVM, ownerContract common.Address, currentHeight uint64, gas *uint64, input []byte) error { // Ensure msg.sender is ownerContract + // Input should be tightly packed 33 bytes + if len(input) != 33 { + return errors.New("input length is not 33 bytes") + } + + // Extract beneficiaryMiner + beneficiaryMiner := common.BytesToAddress(input[:20], evm.chainConfig.Location) + // Extract lockupByte + lockupByte := input[20] + + // Extract epoch + epoch := binary.BigEndian.Uint32(input[21:25]) + + // Extract etxGasLimit + etxGasLimit := binary.BigEndian.Uint64(input[25:33]) + + if *gas < etxGasLimit { + return ErrOutOfGas + } + *gas -= etxGasLimit + + _, err := ownerContract.InternalAndQuaiAddress() + if err != nil { + return err + } + _, err = beneficiaryMiner.InternalAddress() + if err != nil { + return err + } + + balance, trancheUnlockHeight, elements := rawdb.ReadCoinbaseLockup(evm.StateDB.UnderlyingDatabase(), evm.Batch, ownerContract, beneficiaryMiner, lockupByte, epoch) + if trancheUnlockHeight == 0 { + return errors.New("no lockup to claim") + } + if trancheUnlockHeight > uint32(currentHeight) { + return errors.New("tranche is not unlocked yet") + } + if elements == 0 { + return errors.New("no lockup to claim") + } + deletedCoinbaseLockupHash := types.CoinbaseLockupHash(ownerContract, beneficiaryMiner, lockupByte, epoch, balance, trancheUnlockHeight, elements) + coinbaseLockupKey := rawdb.DeleteCoinbaseLockup(evm.Batch, ownerContract, beneficiaryMiner, lockupByte, epoch) + + evm.ETXCacheLock.RLock() + index := len(evm.ETXCache) + evm.ETXCacheLock.RUnlock() + if index > math.MaxUint16 { + return fmt.Errorf("CreateETX overflow error: too many ETXs in cache") + } + + externalTx := types.ExternalTx{Value: balance, To: &beneficiaryMiner, Sender: ownerContract, EtxType: uint64(types.CoinbaseLockupType), OriginatingTxHash: evm.Hash, ETXIndex: uint16(index), Gas: etxGasLimit} + + evm.ETXCacheLock.Lock() + evm.ETXCache = append(evm.ETXCache, types.NewTx(&externalTx)) + evm.CoinbaseDeletedHashes = append(evm.CoinbaseDeletedHashes, &deletedCoinbaseLockupHash) + if err := rawdb.WriteCoinbaseLockupToMap(evm.CoinbasesDeleted, coinbaseLockupKey, balance, trancheUnlockHeight, elements); err != nil { + evm.ETXCacheLock.Unlock() + return err + } + evm.ETXCacheLock.Unlock() + return nil +} + +// AddNewLock adds a new locked balance to the lockup contract +func AddNewLock(statedb StateDB, batch ethdb.Batch, ownerContract common.Address, beneficiaryMiner common.Address, sender common.InternalAddress, lockupByte byte, unlockHeight uint64, epoch uint32, value *big.Int, location common.Location, log_ bool) ([]byte, []byte, *common.Hash, *common.Hash, error) { + _, err := ownerContract.InternalAndQuaiAddress() + if err != nil { + return nil, nil, nil, nil, err + } + _, err = beneficiaryMiner.InternalAddress() + if err != nil { + return nil, nil, nil, nil, err + } + if sender != common.OneInternal(location) { + return nil, nil, nil, nil, errors.New("sender is not the correct internal address") + } + balance, trancheUnlockHeight, elements := rawdb.ReadCoinbaseLockup(statedb.UnderlyingDatabase(), batch, ownerContract, beneficiaryMiner, lockupByte, epoch) + + oldCoinbaseLockupHash_ := types.CoinbaseLockupHash(ownerContract, beneficiaryMiner, lockupByte, epoch, balance, trancheUnlockHeight, elements) + oldCoinbaseLockupHashPtr := &oldCoinbaseLockupHash_ + oldKey := rawdb.CoinbaseLockupKey(ownerContract, beneficiaryMiner, lockupByte, epoch) + oldKey = oldKey[len(rawdb.CoinbaseLockupPrefix):] + if trancheUnlockHeight != 0 && unlockHeight < uint64(trancheUnlockHeight) { + return nil, nil, nil, nil, errors.New("new unlock height is less than the current tranche unlock height, math is broken") + } + if epoch == 0 && trancheUnlockHeight != 0 { + return nil, nil, nil, nil, errors.New("epoch is 0 but trancheUnlockHeight is not") + } + + if trancheUnlockHeight == 0 { + // New epoch: create new lockup tranche, don't change previous one + if epoch+1 > math.MaxUint32 { + return nil, nil, nil, nil, errors.New("epoch overflow") + } + elements = 0 + balance = new(big.Int) + trancheUnlockHeight = uint32(unlockHeight) // TODO: ensure overflow is acceptable here + oldCoinbaseLockupHashPtr = nil + oldKey = nil + statedb.Finalise(true) + if log_ { + log.Global.Info("Rotated epoch: ", " owner: ", ownerContract, " miner: ", beneficiaryMiner, " epoch: ", epoch) + } + } + + elements++ + balance.Add(balance, value) + + newKey, err := rawdb.WriteCoinbaseLockup(batch, ownerContract, beneficiaryMiner, lockupByte, epoch, balance, trancheUnlockHeight, elements) + if err != nil { + return nil, nil, nil, nil, err + } + // Cut off prefix from keys + newKey = newKey[len(rawdb.CoinbaseLockupPrefix):] + + newCoinbaseLockupHash := types.CoinbaseLockupHash(ownerContract, beneficiaryMiner, lockupByte, epoch, balance, trancheUnlockHeight, elements) + if log_ { + log.Global.Info("Added new lockup: ", " contract: ", ownerContract, " miner: ", beneficiaryMiner, " epoch: ", epoch, " balance: ", balance.String(), " value: ", value.String(), " trancheUnlockHeight: ", trancheUnlockHeight, " elements: ", elements, " lockupByte: ", lockupByte) + } + return oldKey, newKey, oldCoinbaseLockupHashPtr, &newCoinbaseLockupHash, nil +} diff --git a/core/vm/evm.go b/core/vm/evm.go index 26318ab20f..ca6420bcb4 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -28,6 +28,9 @@ import ( "github.com/dominant-strategies/go-quai/common" "github.com/dominant-strategies/go-quai/core/types" "github.com/dominant-strategies/go-quai/crypto" + "github.com/dominant-strategies/go-quai/ethdb" + "github.com/dominant-strategies/go-quai/ethdb/memorydb" + "github.com/dominant-strategies/go-quai/log" "github.com/dominant-strategies/go-quai/params" "github.com/holiman/uint256" ) @@ -139,21 +142,31 @@ type EVM struct { // applied in opCall*. callGasTemp uint64 - ETXCache []*types.Transaction - ETXCacheLock sync.RWMutex + ETXCache []*types.Transaction + CoinbaseDeletedHashes []*common.Hash + CoinbasesDeleted map[[47]byte][]byte + ETXCacheLock sync.RWMutex + Batch ethdb.Batch } // NewEVM returns a new EVM. The returned EVM is not thread safe and should // only ever be used *once*. -func NewEVM(blockCtx BlockContext, txCtx TxContext, statedb StateDB, chainConfig *params.ChainConfig, config Config) *EVM { +func NewEVM(blockCtx BlockContext, txCtx TxContext, statedb StateDB, chainConfig *params.ChainConfig, config Config, batch ethdb.Batch) *EVM { evm := &EVM{ - Context: blockCtx, - TxContext: txCtx, - StateDB: statedb, - Config: config, - chainConfig: chainConfig, - chainRules: chainConfig.Rules(blockCtx.BlockNumber), - ETXCache: make([]*types.Transaction, 0), + Context: blockCtx, + TxContext: txCtx, + StateDB: statedb, + Config: config, + chainConfig: chainConfig, + chainRules: chainConfig.Rules(blockCtx.BlockNumber), + ETXCache: make([]*types.Transaction, 0), + CoinbaseDeletedHashes: make([]*common.Hash, 0), + CoinbasesDeleted: make(map[[47]byte][]byte), + } + if batch != nil { + evm.Batch = batch + } else { + evm.Batch = memorydb.New(log.Global).NewBatch() // Just used as a cache for simulating calls } evm.interpreter = NewEVMInterpreter(evm, config) return evm @@ -164,6 +177,7 @@ func NewEVM(blockCtx BlockContext, txCtx TxContext, statedb StateDB, chainConfig func (evm *EVM) Reset(txCtx TxContext, statedb StateDB) { evm.TxContext = txCtx evm.StateDB = statedb + evm.ResetCoinbasesDeleted() } // Cancel cancels any running EVM operation. This may be called concurrently and @@ -198,6 +212,10 @@ func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas if value.Sign() != 0 && !evm.Context.CanTransfer(evm.StateDB, caller.Address(), value) { return nil, gas, 0, ErrInsufficientBalance } + if addr.Equal(LockupContractAddresses[[2]byte(evm.chainConfig.Location)]) { + ret, err := RunLockupContract(evm, caller.Address(), &gas, input) + return ret, gas, 0, err + } snapshot := evm.StateDB.Snapshot() p, isPrecompile, addr := evm.precompile(addr) internalAddr, err := addr.InternalAndQuaiAddress() @@ -689,3 +707,16 @@ func calcEtxFeeMultiplier(fromAddr, toAddr common.Address) *big.Int { // ChainConfig returns the environment's chain configuration func (evm *EVM) ChainConfig() *params.ChainConfig { return evm.chainConfig } + +func (evm *EVM) GetLatestMinerEpoch() (uint32, error) { return evm.StateDB.GetLatestEpoch() } + +func (evm *EVM) ResetCoinbasesDeleted() { + evm.CoinbasesDeleted = make(map[[47]byte][]byte) +} + +func (evm *EVM) UndoCoinbasesDeleted() { + for key, value := range evm.CoinbasesDeleted { + evm.Batch.Put(key[:], value) + } + evm.ResetCoinbasesDeleted() +} diff --git a/core/vm/interface.go b/core/vm/interface.go index 53673a0106..45af3a3396 100644 --- a/core/vm/interface.go +++ b/core/vm/interface.go @@ -21,6 +21,7 @@ import ( "github.com/dominant-strategies/go-quai/common" "github.com/dominant-strategies/go-quai/core/types" + "github.com/dominant-strategies/go-quai/ethdb" ) // StateDB is an EVM database for full state querying. @@ -75,6 +76,11 @@ type StateDB interface { AddPreimage(common.Hash, []byte) ForEachStorage(common.InternalAddress, func(common.Hash, common.Hash) bool) error + UnderlyingDatabase() ethdb.KeyValueReader + + GetLatestEpoch() (uint32, error) + SetLatestEpoch(epoch uint32) error + Finalise(deleteEmptyObjects bool) } // CallContext provides a basic interface for the EVM calling conventions. The EVM diff --git a/core/worker.go b/core/worker.go index 9fa619b234..108df70f73 100644 --- a/core/worker.go +++ b/core/worker.go @@ -31,7 +31,6 @@ import ( "github.com/dominant-strategies/go-quai/trie" lru "github.com/hashicorp/golang-lru/v2" expireLru "github.com/hashicorp/golang-lru/v2/expirable" - "github.com/holiman/uint256" ) const ( @@ -55,12 +54,15 @@ const ( c_uncleCacheSize = 100 ) +var defaultLockupContractAddress = common.HexToAddress("0x004A2e19E838218544eE571a900f62Cb050B39CE", common.Location{0, 0}) + // environment is the worker's current environment and holds all // information of the sealing block generation. type environment struct { signer types.Signer state *state.StateDB // apply state changes here + batch ethdb.Batch // batch to write UTXO and coinbase lockup changes (in memory) ancestors mapset.Set // ancestor set (used for checking uncle parent validity) family mapset.Set // family set (used for checking uncle invalidity) tcount int // tx count in cycle @@ -82,11 +84,15 @@ type environment struct { uncles map[common.Hash]*types.WorkObjectHeader utxosCreate []common.Hash utxosDelete []common.Hash + coinbaseLockupsCreated map[string]common.Hash + coinbaseLockupsDeleted map[string]common.Hash + coinbaseRotatedEpochs map[string]struct{} parentStateSize *big.Int quaiCoinbaseEtxs map[[21]byte]*big.Int deletedUtxos map[common.Hash]struct{} qiGasScalingFactor float64 utxoSetSize uint64 + coinbaseLatestEpoch uint32 } // unclelist returns the contained uncles as the list format. @@ -633,7 +639,8 @@ func (w *worker) GeneratePendingHeader(block *types.WorkObject, fill bool, txs t if err != nil { return nil, err } - lockupByte := work.wo.Lock() + //lockupByte := work.wo.Lock() + lockupData := work.wo.Data() // If the primary coinbase belongs to a ledger and there is no fees // for other ledger, there is no etxs emitted for the other ledger @@ -641,22 +648,23 @@ func (w *worker) GeneratePendingHeader(block *types.WorkObject, fill bool, txs t coinbaseReward := misc.CalculateReward(block, work.wo.WorkObjectHeader()) blockReward := new(big.Int).Add(coinbaseReward, work.quaiFees) primaryCoinbase := w.GetPrimaryCoinbase() - coinbaseEtx := types.NewTx(&types.ExternalTx{To: &primaryCoinbase, Gas: params.TxGas, Value: blockReward, EtxType: types.CoinbaseType, OriginatingTxHash: common.SetBlockHashForQuai(block.Hash(), w.hc.NodeLocation()), ETXIndex: uint16(len(work.etxs)), Sender: primaryCoinbase, Data: []byte{lockupByte}}) + coinbaseEtx := types.NewTx(&types.ExternalTx{To: &primaryCoinbase, Gas: params.TxGas, Value: blockReward, EtxType: types.CoinbaseType, OriginatingTxHash: common.SetBlockHashForQuai(block.Hash(), w.hc.NodeLocation()), ETXIndex: uint16(len(work.etxs)), Sender: primaryCoinbase, Data: lockupData}) work.etxs = append(work.etxs, coinbaseEtx) if work.utxoFees.Cmp(big.NewInt(0)) != 0 { secondaryCoinbase := w.GetSecondaryCoinbase() - coinbaseEtx := types.NewTx(&types.ExternalTx{To: &secondaryCoinbase, Gas: params.TxGas, Value: work.utxoFees, EtxType: types.CoinbaseType, OriginatingTxHash: common.SetBlockHashForQi(block.Hash(), w.hc.NodeLocation()), ETXIndex: uint16(len(work.etxs)), Sender: w.secondaryCoinbase, Data: []byte{lockupByte}}) + // TODO: Perhaps it makes more sense not to send fee rewards to the lockup contract? + coinbaseEtx := types.NewTx(&types.ExternalTx{To: &secondaryCoinbase, Gas: params.TxGas, Value: work.utxoFees, EtxType: types.CoinbaseType, OriginatingTxHash: common.SetBlockHashForQi(block.Hash(), w.hc.NodeLocation()), ETXIndex: uint16(len(work.etxs)), Sender: w.secondaryCoinbase, Data: lockupData}) work.etxs = append(work.etxs, coinbaseEtx) } } else if bytes.Equal(work.wo.PrimaryCoinbase().Bytes(), qiCoinbase.Bytes()) { coinbaseReward := misc.CalculateReward(block, work.wo.WorkObjectHeader()) blockReward := new(big.Int).Add(coinbaseReward, work.utxoFees) primaryCoinbase := w.GetPrimaryCoinbase() - coinbaseEtx := types.NewTx(&types.ExternalTx{To: &primaryCoinbase, Gas: params.TxGas, Value: blockReward, EtxType: types.CoinbaseType, OriginatingTxHash: common.SetBlockHashForQi(block.Hash(), w.hc.NodeLocation()), ETXIndex: uint16(len(work.etxs)), Sender: primaryCoinbase, Data: []byte{lockupByte}}) + coinbaseEtx := types.NewTx(&types.ExternalTx{To: &primaryCoinbase, Gas: params.TxGas, Value: blockReward, EtxType: types.CoinbaseType, OriginatingTxHash: common.SetBlockHashForQi(block.Hash(), w.hc.NodeLocation()), ETXIndex: uint16(len(work.etxs)), Sender: primaryCoinbase, Data: lockupData}) work.etxs = append(work.etxs, coinbaseEtx) if work.quaiFees.Cmp(big.NewInt(0)) != 0 { secondaryCoinbase := w.GetSecondaryCoinbase() - coinbaseEtx := types.NewTx(&types.ExternalTx{To: &secondaryCoinbase, Gas: params.TxGas, Value: work.quaiFees, EtxType: types.CoinbaseType, OriginatingTxHash: common.SetBlockHashForQuai(block.Hash(), w.hc.NodeLocation()), ETXIndex: uint16(len(work.etxs)), Sender: secondaryCoinbase, Data: []byte{lockupByte}}) + coinbaseEtx := types.NewTx(&types.ExternalTx{To: &secondaryCoinbase, Gas: params.TxGas, Value: work.quaiFees, EtxType: types.CoinbaseType, OriginatingTxHash: common.SetBlockHashForQuai(block.Hash(), w.hc.NodeLocation()), ETXIndex: uint16(len(work.etxs)), Sender: secondaryCoinbase, Data: lockupData}) work.etxs = append(work.etxs, coinbaseEtx) } } @@ -671,7 +679,7 @@ func (w *worker) GeneratePendingHeader(block *types.WorkObject, fill bool, txs t } else { originHash = common.SetBlockHashForQi(block.Hash(), w.hc.NodeLocation()) } - work.etxs = append(work.etxs, types.NewTx(&types.ExternalTx{To: &uncleCoinbase, Gas: params.TxGas, Value: reward, EtxType: types.CoinbaseType, OriginatingTxHash: originHash, ETXIndex: uint16(len(work.etxs)), Sender: uncleCoinbase, Data: append([]byte{uncle.Lock()}, uncle.Data()...)})) + work.etxs = append(work.etxs, types.NewTx(&types.ExternalTx{To: &uncleCoinbase, Gas: params.TxGas, Value: reward, EtxType: types.CoinbaseType, OriginatingTxHash: originHash, ETXIndex: uint16(len(work.etxs)), Sender: uncleCoinbase, Data: uncle.Data()})) } } @@ -724,6 +732,7 @@ func (w *worker) GeneratePendingHeader(block *types.WorkObject, fill bool, txs t } if nodeCtx == common.ZONE_CTX && w.hc.ProcessingState() { + work.batch.Reset() if !fromOrderedTransactionSet { select { case w.orderTransactionCh <- transactionOrderingInfo{work.txs, work.gasUsedAfterTransaction, block}: @@ -731,6 +740,9 @@ func (w *worker) GeneratePendingHeader(block *types.WorkObject, fill bool, txs t w.logger.Debug("w.orderTranscationCh is full") } } + for _, hash := range work.coinbaseLockupsCreated { + work.utxosCreate = append(work.utxosCreate, hash) + } } // Create a local environment copy, avoid the data race with snapshot state. @@ -744,6 +756,9 @@ func (w *worker) GeneratePendingHeader(block *types.WorkObject, fill bool, txs t w.printPendingHeaderInfo(work, newWo, start) work.utxosCreate = nil work.utxosDelete = nil + work.coinbaseLockupsCreated = nil + work.coinbaseLockupsDeleted = nil + work.coinbaseRotatedEpochs = nil return newWo, nil } @@ -996,22 +1011,36 @@ func (w *worker) makeEnv(parent *types.WorkObject, proposedWo *types.WorkObject, } // Note the passed coinbase may be different with header.Coinbase. env := &environment{ - signer: types.MakeSigner(w.chainConfig, proposedWo.Number(w.hc.NodeCtx())), - state: state, - primaryCoinbase: primaryCoinbase, - secondaryCoinbase: secondaryCoinbase, - ancestors: mapset.NewSet(), - family: mapset.NewSet(), - wo: proposedWo, - uncles: make(map[common.Hash]*types.WorkObjectHeader), - etxRLimit: etxRLimit, - etxPLimit: etxPLimit, - parentStateSize: quaiStateSize, - quaiCoinbaseEtxs: make(map[[21]byte]*big.Int), - deletedUtxos: make(map[common.Hash]struct{}), - qiGasScalingFactor: math.Log(float64(utxoSetSize)), - utxoSetSize: utxoSetSize, + signer: types.MakeSigner(w.chainConfig, proposedWo.Number(w.hc.NodeCtx())), + state: state, + batch: w.workerDb.NewBatch(), + primaryCoinbase: primaryCoinbase, + secondaryCoinbase: secondaryCoinbase, + ancestors: mapset.NewSet(), + family: mapset.NewSet(), + wo: proposedWo, + uncles: make(map[common.Hash]*types.WorkObjectHeader), + etxRLimit: etxRLimit, + etxPLimit: etxPLimit, + parentStateSize: quaiStateSize, + quaiCoinbaseEtxs: make(map[[21]byte]*big.Int), + deletedUtxos: make(map[common.Hash]struct{}), + qiGasScalingFactor: math.Log(float64(utxoSetSize)), + utxoSetSize: utxoSetSize, + coinbaseLockupsCreated: make(map[string]common.Hash), + coinbaseLockupsDeleted: make(map[string]common.Hash), + coinbaseRotatedEpochs: make(map[string]struct{}), + } + coinbaseLockupEpoch, err := env.state.GetLatestEpoch() + if err != nil { + return nil, fmt.Errorf("could not get latest epoch: %w", err) + } + if proposedWo.NumberU64(common.ZONE_CTX)%params.CoinbaseEpochBlocks == 0 || coinbaseLockupEpoch == 0 { + coinbaseLockupEpoch++ + env.state.SetLatestEpoch(coinbaseLockupEpoch) } + env.coinbaseLatestEpoch = coinbaseLockupEpoch + env.batch.SetPending(true) // Keep track of transactions which return errors so they can be removed env.tcount = 0 return env, nil @@ -1095,72 +1124,115 @@ func (w *worker) commitTransaction(env *environment, parent *types.WorkObject, t } } if tx.To().IsInQiLedgerScope() { // Qi coinbase - var lockup *big.Int - // The first lock up period changes after the fork - if lockupByte == 0 { - if env.wo.NumberU64(common.ZONE_CTX) < params.GoldenAgeForkNumberV1 { - lockup = new(big.Int).SetUint64(params.OldConversionLockPeriod) - if lockup.Uint64() < params.OldConversionLockPeriod { - return nil, false, fmt.Errorf("coinbase lockup period is less than the minimum lockup period of %d blocks", params.OldConversionLockPeriod) - } - } else { - lockup = new(big.Int).SetUint64(params.NewConversionLockPeriod) - if lockup.Uint64() < params.NewConversionLockPeriod { - return nil, false, fmt.Errorf("coinbase lockup period is less than the minimum lockup period of %d blocks", params.NewConversionLockPeriod) - } - } - } else { - lockup = new(big.Int).SetUint64(params.LockupByteToBlockDepth[lockupByte]) + + lockup := new(big.Int).SetUint64(params.LockupByteToBlockDepth[lockupByte]) + if lockup.Uint64() < params.OldConversionLockPeriod { + return nil, false, fmt.Errorf("coinbase lockup period is less than the minimum lockup period of %d blocks", params.OldConversionLockPeriod) } lockup.Add(lockup, env.wo.Number(w.hc.NodeCtx())) value := params.CalculateCoinbaseValueWithLockup(tx.Value(), lockupByte) - denominations := misc.FindMinDenominations(value) - outputIndex := uint16(0) - // Iterate over the denominations in descending order - for denomination := types.MaxDenomination; denomination >= 0; denomination-- { - // If the denomination count is zero, skip it - if denominations[uint8(denomination)] == 0 { - continue + if len(tx.Data()) == 1 { + denominations := misc.FindMinDenominations(value) + outputIndex := uint16(0) + // Iterate over the denominations in descending order + for denomination := types.MaxDenomination; denomination >= 0; denomination-- { + // If the denomination count is zero, skip it + if denominations[uint8(denomination)] == 0 { + continue + } + for j := uint64(0); j < denominations[uint8(denomination)]; j++ { + if outputIndex >= types.MaxOutputIndex { + // No more gas, the rest of the denominations are lost but the tx is still valid + break + } + // the ETX hash is guaranteed to be unique + utxoHash := types.UTXOHash(tx.Hash(), outputIndex, types.NewUtxoEntry(types.NewTxOut(uint8(denomination), tx.To().Bytes(), lockup))) + env.utxosCreate = append(env.utxosCreate, utxoHash) + outputIndex++ + } } - for j := uint64(0); j < denominations[uint8(denomination)]; j++ { - if outputIndex >= types.MaxOutputIndex { - // No more gas, the rest of the denominations are lost but the tx is still valid - break + receipt := &types.Receipt{Type: tx.Type(), Status: types.ReceiptStatusSuccessful, GasUsed: 0, TxHash: tx.Hash()} + gasUsed := env.wo.GasUsed() + if parent.NumberU64(common.ZONE_CTX) >= params.TimeToStartTx { + gasUsed += params.TxGas + } + + env.wo.Header().SetGasUsed(gasUsed) + env.gasUsedAfterTransaction = append(env.gasUsedAfterTransaction, gasUsed) + env.txs = append(env.txs, tx) + env.receipts = append(env.receipts, receipt) + return receipt.Logs, true, nil + } else if len(tx.Data()) == common.AddressLength+1 { + contractAddr := common.BytesToAddress(tx.Data()[1:], w.chainConfig.Location) + internal, err := contractAddr.InternalAndQuaiAddress() + if err != nil { + return nil, false, fmt.Errorf("coinbase tx %x has invalid recipient: %w", tx.Hash(), err) + } + if env.state.GetCode(internal) == nil { + // Coinbase data is either too long or too small + // Coinbase reward is lost + receipt := &types.Receipt{Type: tx.Type(), Status: types.ReceiptStatusFailed, GasUsed: 0, TxHash: tx.Hash()} + env.gasUsedAfterTransaction = append(env.gasUsedAfterTransaction, 0) + env.txs = append(env.txs, tx) + env.receipts = append(env.receipts, receipt) + return []*types.Log{}, false, nil + } + + oldCoinbaseLockupKey, newCoinbaseLockupKey, oldCoinbaseLockupHash, newCoinbaseLockupHash, err := vm.AddNewLock(env.state, env.batch, contractAddr, *tx.To(), common.OneInternal(w.chainConfig.Location), lockupByte, lockup.Uint64(), env.coinbaseLatestEpoch, value, w.chainConfig.Location, false) + if err != nil || newCoinbaseLockupHash == nil { + return nil, false, fmt.Errorf("could not add new lock: %w", err) + } + // Store the new lockup key every time + env.coinbaseLockupsCreated[string(newCoinbaseLockupKey)] = *newCoinbaseLockupHash + + if oldCoinbaseLockupHash != nil { + // We deleted (updated) the old lockup, write it to deleted list but only the first time + if _, exists := env.coinbaseLockupsDeleted[string(oldCoinbaseLockupKey)]; !exists { + if _, exists := env.coinbaseRotatedEpochs[string(newCoinbaseLockupKey)]; !exists { + env.coinbaseLockupsDeleted[string(oldCoinbaseLockupKey)] = *oldCoinbaseLockupHash + env.utxosDelete = append(env.utxosDelete, *oldCoinbaseLockupHash) + } } - // the ETX hash is guaranteed to be unique - utxoHash := types.UTXOHash(tx.Hash(), outputIndex, types.NewUtxoEntry(types.NewTxOut(uint8(denomination), tx.To().Bytes(), lockup))) - env.utxosCreate = append(env.utxosCreate, utxoHash) - outputIndex++ + } else { + // If we did not delete, we are rotating the epoch and need to store it + env.coinbaseRotatedEpochs[string(newCoinbaseLockupKey)] = struct{}{} } + receipt := &types.Receipt{Type: tx.Type(), Status: types.ReceiptStatusSuccessful, GasUsed: 0, TxHash: tx.Hash()} // todo: consider adding the reward to the receipt in a log + gasUsed := env.wo.GasUsed() + if parent.NumberU64(common.ZONE_CTX) >= params.TimeToStartTx { + gasUsed += params.TxGas + } + env.wo.Header().SetGasUsed(gasUsed) + env.gasUsedAfterTransaction = append(env.gasUsedAfterTransaction, gasUsed) + env.txs = append(env.txs, tx) + env.receipts = append(env.receipts, receipt) + return receipt.Logs, true, nil + } else { + // Coinbase data is either too long or too small + // Coinbase reward is lost + receipt := &types.Receipt{Type: tx.Type(), Status: types.ReceiptStatusFailed, GasUsed: 0, TxHash: tx.Hash()} + env.gasUsedAfterTransaction = append(env.gasUsedAfterTransaction, 0) + env.txs = append(env.txs, tx) + env.receipts = append(env.receipts, receipt) + return []*types.Log{}, false, nil } - gasUsed := env.wo.GasUsed() - if parent.NumberU64(common.ZONE_CTX) >= params.TimeToStartTx { - gasUsed += params.TxGas - } - env.wo.Header().SetGasUsed(gasUsed) - env.gasUsedAfterTransaction = append(env.gasUsedAfterTransaction, gasUsed) - env.txs = append(env.txs, tx) - - return []*types.Log{}, false, nil } else if tx.To().IsInQuaiLedgerScope() { // Quai coinbase - internal, err := tx.To().InternalAndQuaiAddress() + _, err := tx.To().InternalAndQuaiAddress() if err != nil { return nil, false, fmt.Errorf("coinbase tx %x has invalid recipient: %w", tx.Hash(), err) } var receipt *types.Receipt - code := env.state.GetCode(internal) - if len(code) == 0 && len(tx.Data()) == 1 { - // No code and coinbase has no extra data + if len(tx.Data()) == 1 { + // Coinbase has no extra data // Coinbase is valid, no gas used receipt = &types.Receipt{Type: tx.Type(), Status: types.ReceiptStatusSuccessful, GasUsed: 0, TxHash: tx.Hash()} env.gasUsedAfterTransaction = append(env.gasUsedAfterTransaction, 0) env.txs = append(env.txs, tx) env.receipts = append(env.receipts, receipt) return []*types.Log{}, false, nil - } else if (len(code) == 0 && len(tx.Data()) > 1) || (len(code) > 0 && len(tx.Data()) < 5) { - // No code, but coinbase has extra data - // Or there is code, but coinbase data doesn't include 4-byte function sig + } else if len(tx.Data()) != common.AddressLength+1 { + // Coinbase data is either too long or too small // Coinbase reward is lost receipt = &types.Receipt{Type: tx.Type(), Status: types.ReceiptStatusFailed, GasUsed: 0, TxHash: tx.Hash()} env.gasUsedAfterTransaction = append(env.gasUsedAfterTransaction, 0) @@ -1174,43 +1246,51 @@ func (w *worker) commitTransaction(env *environment, parent *types.WorkObject, t return nil, false, fmt.Errorf("coinbase lockup period is less than the minimum lockup period of %d blocks", params.OldConversionLockPeriod) } lockup.Add(lockup, env.wo.Number(w.hc.NodeCtx())) - sig := tx.Data()[1:5] - data := make([]byte, 0, 0) - data = append(data, sig...) - if len(tx.Data()) > 5 { - // If there is extra data, append it to the data (for example, reward beneficiary) - data = append(data, tx.Data()[5:]...) - } - reward, overflow := uint256.FromBig(params.CalculateCoinbaseValueWithLockup(tx.Value(), lockupByte)) - if overflow { - return nil, false, fmt.Errorf("coinbase value overflow") - } - temp := reward.Bytes32() - data = append(data, temp[:]...) - lockup256, overflow := uint256.FromBig(lockup) - if overflow { - return nil, false, fmt.Errorf("coinbase lockup overflow") - } - temp = lockup256.Bytes32() - data = append(data, temp[:]...) - - gp := new(types.GasPool).AddGas(params.CoinbaseGas) - gasUsed := env.wo.GasUsed() - stateUsed := env.wo.StateUsed() - // This lockup contract transaction comes from the 0x000...1 address, so the contract should only accept calls from this address - receipt, _, err = ApplyCoinbaseLockupTransaction(w.chainConfig, parent, *env.parentOrder, w.hc, &env.primaryCoinbase, gp, env.state, env.wo, tx, &gasUsed, &stateUsed, *w.hc.bc.processor.GetVMConfig(), &env.etxRLimit, &env.etxPLimit, data, w.logger) + contractAddr := common.BytesToAddress(tx.Data()[1:], w.chainConfig.Location) + internal, err := contractAddr.InternalAndQuaiAddress() if err != nil { - return nil, false, fmt.Errorf("could not apply coinbase tx %v: %w", tx.Hash().Hex(), err) + return nil, false, fmt.Errorf("coinbase tx %x has invalid recipient: %w", tx.Hash(), err) + } + + if env.state.GetCode(internal) == nil { + // Coinbase data is either too long or too small + // Coinbase reward is lost + receipt := &types.Receipt{Type: tx.Type(), Status: types.ReceiptStatusFailed, GasUsed: 0, TxHash: tx.Hash()} + env.gasUsedAfterTransaction = append(env.gasUsedAfterTransaction, 0) + env.txs = append(env.txs, tx) + env.receipts = append(env.receipts, receipt) + return []*types.Log{}, false, nil + } + + reward := params.CalculateCoinbaseValueWithLockup(tx.Value(), lockupByte) + // Add the lockup owned by the smart contract with the miner as beneficiary + oldCoinbaseLockupKey, newCoinbaseLockupKey, oldCoinbaseLockupHash, newCoinbaseLockupHash, err := vm.AddNewLock(env.state, env.batch, contractAddr, *tx.To(), common.OneInternal(w.chainConfig.Location), lockupByte, lockup.Uint64(), env.coinbaseLatestEpoch, reward, w.chainConfig.Location, false) + if err != nil || newCoinbaseLockupHash == nil { + return nil, false, fmt.Errorf("could not add new lock: %w", err) } - if len(receipt.OutboundEtxs) > 0 { - return nil, false, fmt.Errorf("coinbase tx %x has outbound etxs", tx.Hash()) + // Store the new lockup key every time + env.coinbaseLockupsCreated[string(newCoinbaseLockupKey)] = *newCoinbaseLockupHash + + if oldCoinbaseLockupHash != nil { + // We deleted (updated) the old lockup, write it to deleted list but only the first time + if _, exists := env.coinbaseLockupsDeleted[string(oldCoinbaseLockupKey)]; !exists { + if _, exists := env.coinbaseRotatedEpochs[string(newCoinbaseLockupKey)]; !exists { + env.coinbaseLockupsDeleted[string(oldCoinbaseLockupKey)] = *oldCoinbaseLockupHash + env.utxosDelete = append(env.utxosDelete, *oldCoinbaseLockupHash) + } + } + } else { + // If we did not delete, we are rotating the epoch and need to store it + env.coinbaseRotatedEpochs[string(newCoinbaseLockupKey)] = struct{}{} } + receipt = &types.Receipt{Type: tx.Type(), Status: types.ReceiptStatusSuccessful, GasUsed: 0, TxHash: tx.Hash()} // todo: consider adding the reward to the receipt in a log + + gasUsed := env.wo.GasUsed() if parent.NumberU64(common.ZONE_CTX) >= params.TimeToStartTx { gasUsed += params.TxGas } env.wo.Header().SetGasUsed(gasUsed) env.gasUsedAfterTransaction = append(env.gasUsedAfterTransaction, gasUsed) - env.wo.Header().SetStateUsed(stateUsed) env.receipts = append(env.receipts, receipt) env.txs = append(env.txs, tx) @@ -1219,6 +1299,56 @@ func (w *worker) commitTransaction(env *environment, parent *types.WorkObject, t } if tx.Type() == types.ExternalTxType && tx.To().IsInQiLedgerScope() { gasUsed := env.wo.GasUsed() + if tx.EtxType() == types.CoinbaseLockupType { + // This is either an unlocked Qi coinbase that was redeemed or Wrapped Qi + // An unlocked/redeemed Quai coinbase ETX is processed below as a standard Quai ETX + if tx.To().IsInQiLedgerScope() { + txGas := tx.Gas() + denominations := misc.FindMinDenominations(tx.Value()) + total := big.NewInt(0) + outputIndex := uint16(0) + success := true + // Iterate over the denominations in descending order + for denomination := types.MaxDenomination; denomination >= 0; denomination-- { + // If the denomination count is zero, skip it + if denominations[uint8(denomination)] == 0 { + continue + } + + for j := uint64(0); j < denominations[uint8(denomination)]; j++ { + if txGas < params.CallValueTransferGas || outputIndex >= types.MaxOutputIndex { + // No more gas, the rest of the denominations are lost but the tx is still valid + success = false + break + } + txGas -= params.CallValueTransferGas + if err := env.gasPool.SubGas(params.CallValueTransferGas); err != nil { + return nil, false, err + } + gasUsed += params.CallValueTransferGas + utxo := types.NewUtxoEntry(types.NewTxOut(uint8(denomination), tx.To().Bytes(), big.NewInt(0))) + env.utxosCreate = append(env.utxosCreate, types.UTXOHash(tx.Hash(), outputIndex, utxo)) + total.Add(total, types.Denominations[uint8(denomination)]) + outputIndex++ + } + } + receipt := &types.Receipt{Type: tx.Type(), Status: types.ReceiptStatusSuccessful, GasUsed: tx.Gas() - txGas, TxHash: tx.Hash(), + Logs: []*types.Log{{ + Address: *tx.To(), + Topics: []common.Hash{types.QuaiToQiConversionTopic}, + Data: total.Bytes(), + }}, + } + if !success { + receipt.Status = types.ReceiptStatusFailed + receipt.GasUsed = tx.Gas() + } + env.wo.Header().SetGasUsed(gasUsed) + env.txs = append(env.txs, tx) + env.gasUsedAfterTransaction = append(env.gasUsedAfterTransaction, gasUsed) + return receipt.Logs, true, nil + } + } if tx.ETXSender().Location().Equal(*tx.To().Location()) { // Quai->Qi conversion txGas := tx.Gas() var lockup *big.Int @@ -1276,23 +1406,16 @@ func (w *worker) commitTransaction(env *environment, parent *types.WorkObject, t outputIndex++ } } - var receipt *types.Receipt - if success { - receipt = &types.Receipt{Type: tx.Type(), Status: types.ReceiptStatusSuccessful, GasUsed: tx.Gas() - txGas, TxHash: tx.Hash(), - Logs: []*types.Log{{ - Address: *tx.To(), - Topics: []common.Hash{types.QuaiToQiConversionTopic}, - Data: total.Bytes(), - }}, - } - } else { - receipt = &types.Receipt{Type: tx.Type(), Status: types.ReceiptStatusFailed, GasUsed: tx.Gas(), TxHash: tx.Hash(), - Logs: []*types.Log{{ - Address: *tx.To(), - Topics: []common.Hash{types.QuaiToQiConversionTopic}, - Data: total.Bytes(), - }}, - } + receipt := &types.Receipt{Type: tx.Type(), Status: types.ReceiptStatusSuccessful, GasUsed: tx.Gas() - txGas, TxHash: tx.Hash(), + Logs: []*types.Log{{ + Address: *tx.To(), + Topics: []common.Hash{types.QuaiToQiConversionTopic}, + Data: total.Bytes(), + }}, + } + if !success { + receipt.Status = types.ReceiptStatusFailed + receipt.GasUsed = tx.Gas() } env.wo.Header().SetGasUsed(gasUsed) env.txs = append(env.txs, tx) @@ -1324,7 +1447,7 @@ func (w *worker) commitTransaction(env *environment, parent *types.WorkObject, t // retrieve the gas used int and pass in the reference to the ApplyTransaction gasUsed := env.wo.GasUsed() stateUsed := env.wo.StateUsed() - receipt, quaiFees, err := ApplyTransaction(w.chainConfig, parent, *env.parentOrder, w.hc, &env.primaryCoinbase, env.gasPool, env.state, env.wo, tx, &gasUsed, &stateUsed, *w.hc.bc.processor.GetVMConfig(), &env.etxRLimit, &env.etxPLimit, w.logger) + receipt, quaiFees, err := ApplyTransaction(w.chainConfig, parent, *env.parentOrder, w.hc, &env.primaryCoinbase, env.gasPool, env.state, env.wo, tx, &gasUsed, &stateUsed, *w.hc.bc.processor.GetVMConfig(), &env.etxRLimit, &env.etxPLimit, env.batch, w.logger) if err != nil { w.logger.WithFields(log.Fields{ "err": err, @@ -1582,7 +1705,10 @@ func (w *worker) prepareWork(genParams *generateParams, wo *types.WorkObject) (* num := parent.Number(nodeCtx) newWo := types.EmptyWorkObject(nodeCtx) newWo.WorkObjectHeader().SetLock(w.GetLockupByte()) - newWo.WorkObjectHeader().SetData([]byte{}) + data := make([]byte, 0, 21) + data = append(data, w.GetLockupByte()) + data = append(data, defaultLockupContractAddress.Bytes()...) + newWo.WorkObjectHeader().SetData(data) newWo.SetParentHash(wo.Hash(), nodeCtx) if w.hc.IsGenesisHash(parent.Hash()) { newWo.SetNumber(big.NewInt(1), nodeCtx) @@ -1770,7 +1896,7 @@ func (w *worker) prepareWork(genParams *generateParams, wo *types.WorkObject) (* if err != nil { return nil, err } - vmenv := vm.NewEVM(blockContext, vm.TxContext{}, env.state, w.chainConfig, *w.hc.bc.processor.GetVMConfig()) + vmenv := vm.NewEVM(blockContext, vm.TxContext{}, env.state, w.chainConfig, *w.hc.bc.processor.GetVMConfig(), env.batch) if _, err := RedeemLockedQuai(w.hc, proposedWo, parent, env.state, vmenv); err != nil { w.logger.WithField("err", err).Error("Failed to redeem locked Quai") return nil, err diff --git a/ethdb/batch.go b/ethdb/batch.go index 1353693318..933ef95cfd 100644 --- a/ethdb/batch.go +++ b/ethdb/batch.go @@ -36,6 +36,10 @@ type Batch interface { // Replay replays the batch contents. Replay(w KeyValueWriter) error + + SetPending(pending bool) + + GetPending(key []byte) (bool, []byte) } // Batcher wraps the NewBatch method of a backing data store. diff --git a/ethdb/leveldb/leveldb.go b/ethdb/leveldb/leveldb.go index 13d1e80c92..203e10f55c 100644 --- a/ethdb/leveldb/leveldb.go +++ b/ethdb/leveldb/leveldb.go @@ -411,16 +411,24 @@ func (db *Database) meter(refresh time.Duration) { // batch is a write-only leveldb batch that commits changes to its host database // when Write is called. A batch cannot be used concurrently. type batch struct { - db *leveldb.DB - b *leveldb.Batch - size int - logger *log.Logger + db *leveldb.DB + b *leveldb.Batch + size int + logger *log.Logger + setPending bool + pending map[string]*[]byte + pendingLock sync.RWMutex } // Put inserts the given value into the batch for later committing. func (b *batch) Put(key, value []byte) error { b.b.Put(key, value) b.size += len(value) + if b.setPending { + b.pendingLock.Lock() + b.pending[string(key)] = &value + b.pendingLock.Unlock() + } return nil } @@ -428,9 +436,32 @@ func (b *batch) Put(key, value []byte) error { func (b *batch) Delete(key []byte) error { b.b.Delete(key) b.size += len(key) + if b.setPending { + b.pendingLock.Lock() + b.pending[string(key)] = nil + b.pendingLock.Unlock() + } return nil } +func (b *batch) GetPending(key []byte) (bool, []byte) { + b.pendingLock.RLock() + defer b.pendingLock.RUnlock() + if val, ok := b.pending[string(key)]; ok { + if val == nil { + return true, nil + } + return false, *val + } + return false, nil +} + +// SetPending must be called for the batch to keep track of pending writes outside of leveldb. +func (b *batch) SetPending(val bool) { + b.pending = make(map[string]*[]byte) + b.setPending = val +} + // ValueSize retrieves the amount of data queued up for writing. func (b *batch) ValueSize() int { return b.size @@ -438,6 +469,8 @@ func (b *batch) ValueSize() int { // Write flushes any accumulated data to disk. func (b *batch) Write() error { + b.pending = nil + b.setPending = false return b.db.Write(b.b, nil) } @@ -445,6 +478,8 @@ func (b *batch) Write() error { func (b *batch) Reset() { b.b.Reset() b.size = 0 + b.pending = nil + b.setPending = false } // Replay replays the batch contents. diff --git a/ethdb/memorydb/memorydb.go b/ethdb/memorydb/memorydb.go index 0651009fc5..3e60d4dcad 100644 --- a/ethdb/memorydb/memorydb.go +++ b/ethdb/memorydb/memorydb.go @@ -277,6 +277,12 @@ func (b *batch) Logger() *log.Logger { return b.db.logger } +func (b *batch) SetPending(pending bool) {} + +func (b *batch) GetPending(key []byte) (bool, []byte) { + return false, nil +} + // iterator can walk over the (potentially partial) keyspace of a memory key // value store. Internally it is a deep copy of the entire iterated state, // sorted by keys. diff --git a/ethdb/pebble/pebble.go b/ethdb/pebble/pebble.go index 2b6b07a68b..1e2dea0989 100644 --- a/ethdb/pebble/pebble.go +++ b/ethdb/pebble/pebble.go @@ -475,6 +475,12 @@ func (b *batch) Logger() *log.Logger { return b.db.logger } +func (b *batch) SetPending(pending bool) {} + +func (b *batch) GetPending(key []byte) (bool, []byte) { + return false, nil +} + // pebbleIterator is a wrapper of underlying iterator in storage engine. // The purpose of this structure is to implement the missing APIs. type pebbleIterator struct { diff --git a/params/protocol_params.go b/params/protocol_params.go index 154bff05ba..e6ff83c3e2 100644 --- a/params/protocol_params.go +++ b/params/protocol_params.go @@ -154,7 +154,7 @@ const ( ConversionConfirmationContext = common.PRIME_CTX // A conversion requires a single coincident Dom confirmation QiToQuaiConversionGas = 100000 // The gas used to convert Qi to Quai DefaultCoinbaseLockup = 0 // The default lockup byte for coinbase rewards - CoinbaseGas = 80000 // Gas given to a coinbase transaction + MaxCoinbaseTrancheElements = 100 // Maximum number of elements in a coinbase tranche ) var ( @@ -193,6 +193,8 @@ var ( MinBaseFeeInQits = big.NewInt(5) OneOverBaseFeeControllerAlpha = big.NewInt(100) BaseFeeMultiplier = big.NewInt(50) + + CoinbaseEpochBlocks uint64 = 100 // Maximum number of blocks in a coinbase tranche ) const ( diff --git a/quai/api_backend.go b/quai/api_backend.go index 18503a2e63..ee4ba81cd5 100644 --- a/quai/api_backend.go +++ b/quai/api_backend.go @@ -293,7 +293,7 @@ func (b *QuaiAPIBackend) GetEVM(ctx context.Context, msg core.Message, state *st if err != nil { return nil, vmError, err } - return vm.NewEVM(context, txContext, state, b.quai.core.Config(), *vmConfig), vmError, nil + return vm.NewEVM(context, txContext, state, b.quai.core.Config(), *vmConfig, nil), vmError, nil } func (b *QuaiAPIBackend) SubscribeRemovedLogsEvent(ch chan<- core.RemovedLogsEvent) event.Subscription {