From 6ed607270d4c1091b2490088ab85309d7e7c7bae Mon Sep 17 00:00:00 2001 From: Jonathan Downing Date: Fri, 12 Apr 2024 16:40:30 -0500 Subject: [PATCH 1/3] Check if Qi address is in-scope before adding to UTXO set --- common/address.go | 42 +++++++++++++++++++++++++++++++++++++++++ core/genesis.go | 4 +++- core/state/statedb.go | 7 ++++++- core/state_processor.go | 12 +++++++++--- core/worker.go | 14 +++++++++++--- 5 files changed, 71 insertions(+), 8 deletions(-) diff --git a/common/address.go b/common/address.go index 067561f73b..c894f476ba 100644 --- a/common/address.go +++ b/common/address.go @@ -75,6 +75,36 @@ func (a Address) InternalAndQuaiAddress() (InternalAddress, error) { return *internal, nil } +func CheckIfBytesAreInternalAndQiAddress(b []byte, nodeLocation Location) error { + if len(b) != AddressLength { + return fmt.Errorf("address %s is not %d bytes long", hexutil.Encode(b), AddressLength) + } + if !IsInChainScope(b, nodeLocation) { + return ErrInvalidScope + } + if AddressBytes(b).IsInQuaiLedgerScope() { + return ErrQuaiAddress + } + return nil +} + +func (a Address) InternalAndQiAddress() (InternalAddress, error) { + if a.inner == nil { + return InternalAddress{}, ErrNilInner + } + if a.IsInQuaiLedgerScope() { + return InternalAddress{}, ErrQuaiAddress + } + internal, ok := a.inner.(*InternalAddress) + if !ok { + return InternalAddress{}, ErrInvalidScope + } + if internal == nil { + return InternalAddress{}, ErrNilInner + } + return *internal, nil +} + func (a Address) IsInQiLedgerScope() bool { // The first bit of the second byte is set if the address is in the Qi ledger return a.Bytes()[1] > 127 @@ -358,6 +388,18 @@ func (a AddressBytes) hex() []byte { return buf[:] } +func (a AddressBytes) Location() *Location { + // Extract nibbles + lowerNib := a[0] & 0x0F // Lower 4 bits + upperNib := (a[0] & 0xF0) >> 4 // Upper 4 bits, shifted right + return &Location{upperNib, lowerNib} +} + +func (a AddressBytes) IsInQuaiLedgerScope() bool { + // The first bit of the second byte is not set if the address is in the Quai ledger + return a[1] <= 127 +} + func MakeErrQiAddress(addr string) error { return fmt.Errorf("address %s is in Qi ledger scope and is not in Quai ledger scope", addr) } diff --git a/core/genesis.go b/core/genesis.go index fe039e5236..3362464c7e 100644 --- a/core/genesis.go +++ b/core/genesis.go @@ -533,6 +533,8 @@ func AddGenesisUtxos(state *state.StateDB, nodeLocation common.Location, logger Denomination: uint8(utxo.Denomination), } - state.CreateUTXO(hash, uint16(utxo.Index), newUtxo) + if err := state.CreateUTXO(hash, uint16(utxo.Index), newUtxo); err != nil { + panic(fmt.Sprintf("Failed to create genesis UTXO: %v", err)) + } } } diff --git a/core/state/statedb.go b/core/state/statedb.go index 39f5ca8844..5dfae106d2 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -575,10 +575,14 @@ func (s *StateDB) DeleteUTXO(txHash common.Hash, outputIndex uint16) { } // CreateUTXO explicitly creates a UTXO entry. -func (s *StateDB) CreateUTXO(txHash common.Hash, outputIndex uint16, utxo *types.UtxoEntry) { +func (s *StateDB) CreateUTXO(txHash common.Hash, outputIndex uint16, utxo *types.UtxoEntry) error { if metrics_config.MetricsEnabled() { defer func(start time.Time) { stateMetrics.WithLabelValues("CreateUTXO").Add(float64(time.Since(start))) }(time.Now()) } + // This check is largely redundant, but it's a good sanity check. Might be removed in the future. + if err := common.CheckIfBytesAreInternalAndQiAddress(utxo.Address, s.nodeLocation); err != nil { + return err + } data, err := rlp.EncodeToBytes(utxo) if err != nil { panic(fmt.Errorf("can't encode UTXO entry at %x: %v", txHash, err)) @@ -586,6 +590,7 @@ func (s *StateDB) CreateUTXO(txHash common.Hash, outputIndex uint16, utxo *types if err := s.utxoTrie.TryUpdate(utxoKey(txHash, outputIndex), data); err != nil { s.setError(fmt.Errorf("createUTXO (%x) error: %v", txHash, err)) } + return nil } func (s *StateDB) CommitUTXOs() (common.Hash, error) { diff --git a/core/state_processor.go b/core/state_processor.go index 82eee754ca..ebe5e78cfa 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -309,7 +309,9 @@ func (p *StateProcessor) Process(block *types.WorkObject, etxSet *types.EtxSet) return nil, nil, nil, nil, 0, fmt.Errorf("etx %032x emits UTXO with value %d greater than max denomination", tx.Hash(), tx.Value().Int64()) } // There are no more checks to be made as the ETX is worked so add it to the set - statedb.CreateUTXO(tx.OriginatingTxHash(), tx.ETXIndex(), types.NewUtxoEntry(types.NewTxOut(uint8(tx.Value().Int64()), tx.To().Bytes()))) + if err := statedb.CreateUTXO(tx.OriginatingTxHash(), tx.ETXIndex(), types.NewUtxoEntry(types.NewTxOut(uint8(tx.Value().Int64()), tx.To().Bytes()))); err != nil { + return nil, nil, nil, nil, 0, fmt.Errorf("could not create UTXO for etx %032x: %w", tx.Hash(), err) + } if err := gp.SubGas(params.CallValueTransferGas); err != nil { return nil, nil, nil, nil, 0, err } @@ -391,7 +393,9 @@ func (p *StateProcessor) Process(block *types.WorkObject, etxSet *types.EtxSet) return nil, nil, nil, nil, 0, fmt.Errorf("coinbase tx emits UTXO with To address not equal to block coinbase") } utxo := types.NewUtxoEntry(&txOut) - statedb.CreateUTXO(coinbaseTx.Hash(), uint16(txOutIdx), utxo) + if err := statedb.CreateUTXO(coinbaseTx.Hash(), uint16(txOutIdx), utxo); err != nil { + return nil, nil, nil, nil, 0, fmt.Errorf("could not create UTXO for coinbase tx %032x: %w", coinbaseTx.Hash(), err) + } p.logger.WithFields(log.Fields{ "txHash": coinbaseTx.Hash().Hex(), "txOutIdx": txOutIdx, @@ -640,7 +644,9 @@ func ProcessQiTx(tx *types.Transaction, chain ChainContext, updateState bool, cu // This output creates a normal UTXO utxo := types.NewUtxoEntry(&txOut) if updateState { - statedb.CreateUTXO(tx.Hash(), uint16(txOutIdx), utxo) + if err := statedb.CreateUTXO(tx.Hash(), uint16(txOutIdx), utxo); err != nil { + return nil, nil, err + } } } } diff --git a/core/worker.go b/core/worker.go index a0043d90dc..d71ac1e32e 100644 --- a/core/worker.go +++ b/core/worker.go @@ -742,7 +742,9 @@ func (w *worker) commitTransaction(env *environment, parent *types.WorkObject, t if tx.Value().Int64() > types.MaxDenomination { return nil, fmt.Errorf("tx %032x emits UTXO with value greater than max denomination", tx.Hash()) } - env.state.CreateUTXO(tx.OriginatingTxHash(), tx.ETXIndex(), types.NewUtxoEntry(types.NewTxOut(uint8(tx.Value().Int64()), tx.To().Bytes()))) + if err := env.state.CreateUTXO(tx.OriginatingTxHash(), tx.ETXIndex(), types.NewUtxoEntry(types.NewTxOut(uint8(tx.Value().Int64()), tx.To().Bytes()))); err != nil { + return nil, err + } gasUsed := env.wo.Header().GasUsed() gasUsed += params.CallValueTransferGas env.wo.Header().SetGasUsed(gasUsed) @@ -1481,7 +1483,11 @@ func (w *worker) processQiTx(tx *types.Transaction, env *environment) error { env.state.DeleteUTXO(utxo.TxHash, utxo.Index) } for outPoint, utxo := range utxosCreate { - env.state.CreateUTXO(outPoint.TxHash, outPoint.Index, utxo) + if err := env.state.CreateUTXO(outPoint.TxHash, outPoint.Index, utxo); err != nil { + // This should never happen and will invalidate the block + log.Global.Errorf("Failed to create UTXO %032x:%d: %v", outPoint.TxHash, outPoint.Index, err) + return err + } } // We could add signature verification here, but it's already checked in the mempool and the signature can't be changed, so duplication is largely unnecessary return nil @@ -1540,7 +1546,9 @@ func createQiCoinbaseTxWithFees(header *types.WorkObject, fees *big.Int, state * tx := types.NewTx(qiTx) for i, out := range qiTx.TxOut { // this may be unnecessary - state.CreateUTXO(tx.Hash(), uint16(i), types.NewUtxoEntry(&out)) + if err := state.CreateUTXO(tx.Hash(), uint16(i), types.NewUtxoEntry(&out)); err != nil { + return nil, err + } } return tx, nil } From 158b9cc3c64fc753d7f1840a5677a4d2e55f124a Mon Sep 17 00:00:00 2001 From: Jonathan Downing Date: Thu, 21 Mar 2024 16:59:19 -0500 Subject: [PATCH 2/3] Added conversion processing with ETX to origin and destination --- common/proto_common.pb.go | 4 +- consensus/blake3pow/consensus.go | 9 +- consensus/misc/rewards.go | 2 +- consensus/progpow/consensus.go | 8 ++ core/rawdb/db.pb.go | 4 +- core/state/statedb.go | 4 +- core/state_processor.go | 192 ++++++++++++++++++++++++------- core/state_transition.go | 13 ++- core/tx_pool.go | 13 +-- core/{ => types}/gaspool.go | 5 +- core/types/proto_block.pb.go | 38 +++--- core/types/proto_block.proto | 1 + core/types/qi_tx.go | 1 + core/types/transaction.go | 22 +++- core/types/utxo.go | 42 ++++++- core/types/utxo_viewpoint.go | 30 ----- core/types/wo.go | 4 +- core/vm/contracts.go | 175 +++++++++++++++++++++++++++- core/vm/evm.go | 44 ++++++- core/vm/instructions.go | 2 +- core/worker.go | 161 ++++++++++++++++++++------ internal/quaiapi/api.go | 9 +- p2p/pb/quai_messages.pb.go | 4 +- params/protocol_params.go | 7 ++ trie/proto_trienode.pb.go | 4 +- 25 files changed, 629 insertions(+), 169 deletions(-) rename core/{ => types}/gaspool.go (96%) delete mode 100644 core/types/utxo_viewpoint.go diff --git a/common/proto_common.pb.go b/common/proto_common.pb.go index bb1508d011..30b8fc2958 100644 --- a/common/proto_common.pb.go +++ b/common/proto_common.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.30.0 -// protoc v4.25.1 +// protoc-gen-go v1.32.0 +// protoc v4.25.2 // source: common/proto_common.proto package common diff --git a/consensus/blake3pow/consensus.go b/consensus/blake3pow/consensus.go index 892e3f34b2..791e3ef8de 100644 --- a/consensus/blake3pow/consensus.go +++ b/consensus/blake3pow/consensus.go @@ -14,6 +14,7 @@ import ( "github.com/dominant-strategies/go-quai/core" "github.com/dominant-strategies/go-quai/core/state" "github.com/dominant-strategies/go-quai/core/types" + "github.com/dominant-strategies/go-quai/core/vm" "github.com/dominant-strategies/go-quai/params" "github.com/dominant-strategies/go-quai/trie" "modernc.org/mathutil" @@ -549,6 +550,13 @@ func (blake3pow *Blake3pow) Finalize(chain consensus.ChainHeaderReader, header * nodeCtx := blake3pow.config.NodeLocation.Context() if nodeCtx == common.ZONE_CTX && chain.IsGenesisHash(header.ParentHash(nodeCtx)) { + // Create the lockup contract account + lockupContract, err := vm.LockupContractAddresses[[2]byte{nodeLocation[0], nodeLocation[1]}].InternalAndQuaiAddress() + if err != nil { + panic(err) + } + state.CreateAccount(lockupContract) + alloc := core.ReadGenesisAlloc("genallocs/gen_alloc_"+nodeLocation.Name()+".json", blake3pow.logger) blake3pow.logger.WithField("alloc", len(alloc)).Info("Allocating genesis accounts") @@ -570,7 +578,6 @@ func (blake3pow *Blake3pow) Finalize(chain consensus.ChainHeaderReader, header * continue } } - core.AddGenesisUtxos(state, nodeLocation, blake3pow.logger) } header.Header().SetUTXORoot(state.UTXORoot()) diff --git a/consensus/misc/rewards.go b/consensus/misc/rewards.go index 58387a7bd3..8134a611b5 100644 --- a/consensus/misc/rewards.go +++ b/consensus/misc/rewards.go @@ -43,7 +43,7 @@ func FindMinDenominations(reward *big.Int) map[uint8]uint8 { amount := new(big.Int).Set(reward) // Iterate over the denominations in descending order (by key) - for i := 15; i >= 0; i-- { + for i := types.MaxDenomination; i >= 0; i-- { denom := types.Denominations[uint8(i)] // Calculate the number of times the denomination fits into the remaining amount diff --git a/consensus/progpow/consensus.go b/consensus/progpow/consensus.go index 35cb1ba45e..5251732484 100644 --- a/consensus/progpow/consensus.go +++ b/consensus/progpow/consensus.go @@ -15,6 +15,7 @@ import ( "github.com/dominant-strategies/go-quai/core" "github.com/dominant-strategies/go-quai/core/state" "github.com/dominant-strategies/go-quai/core/types" + "github.com/dominant-strategies/go-quai/core/vm" "github.com/dominant-strategies/go-quai/params" "github.com/dominant-strategies/go-quai/trie" "modernc.org/mathutil" @@ -585,6 +586,13 @@ func (progpow *Progpow) Finalize(chain consensus.ChainHeaderReader, header *type nodeCtx := progpow.NodeLocation().Context() if nodeCtx == common.ZONE_CTX && chain.IsGenesisHash(header.ParentHash(nodeCtx)) { + // Create the lockup contract account + lockupContract, err := vm.LockupContractAddresses[[2]byte{nodeLocation[0], nodeLocation[1]}].InternalAndQuaiAddress() + if err != nil { + panic(err) + } + state.CreateAccount(lockupContract) + alloc := core.ReadGenesisAlloc("genallocs/gen_alloc_"+nodeLocation.Name()+".json", progpow.logger) progpow.logger.WithField("alloc", len(alloc)).Info("Allocating genesis accounts") diff --git a/core/rawdb/db.pb.go b/core/rawdb/db.pb.go index 30fb67b07d..0f2560b344 100644 --- a/core/rawdb/db.pb.go +++ b/core/rawdb/db.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.30.0 -// protoc v4.25.1 +// protoc-gen-go v1.32.0 +// protoc v4.25.2 // source: core/rawdb/db.proto package rawdb diff --git a/core/state/statedb.go b/core/state/statedb.go index 5dfae106d2..6d948079b2 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -297,7 +297,6 @@ func (s *StateDB) GetBalance(addr common.InternalAddress) *big.Int { } return common.Big0 } - func (s *StateDB) GetNonce(addr common.InternalAddress) uint64 { stateObject := s.getStateObject(addr) if stateObject != nil { @@ -583,6 +582,9 @@ func (s *StateDB) CreateUTXO(txHash common.Hash, outputIndex uint16, utxo *types if err := common.CheckIfBytesAreInternalAndQiAddress(utxo.Address, s.nodeLocation); err != nil { return err } + if utxo.Denomination > types.MaxDenomination { // sanity check + return fmt.Errorf("tx %032x emits UTXO with value %d greater than max denomination", txHash, utxo.Denomination) + } data, err := rlp.EncodeToBytes(utxo) if err != nil { panic(fmt.Errorf("can't encode UTXO entry at %x: %v", txHash, err)) diff --git a/core/state_processor.go b/core/state_processor.go index ebe5e78cfa..c84d3d36ec 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -208,7 +208,7 @@ func (p *StateProcessor) Process(block *types.WorkObject, etxSet *types.EtxSet) nodeCtx = p.hc.NodeCtx() blockNumber = block.Number(nodeCtx) allLogs []*types.Log - gp = new(GasPool).AddGas(block.GasLimit()) + gp = new(types.GasPool).AddGas(block.GasLimit()) ) start := time.Now() parent := p.hc.GetBlock(block.ParentHash(nodeCtx), block.NumberU64(nodeCtx)-1) @@ -279,7 +279,9 @@ func (p *StateProcessor) Process(block *types.WorkObject, etxSet *types.EtxSet) if err != nil { return nil, nil, nil, nil, 0, fmt.Errorf("could not apply tx %d [%v]: %w", i, tx.Hash().Hex(), err) } - qiEtxs = append(qiEtxs, etxs...) + for _, etx := range etxs { + qiEtxs = append(qiEtxs, types.NewTx(etx)) + } totalFees.Add(totalFees, fees) continue } @@ -305,23 +307,69 @@ func (p *StateProcessor) Process(block *types.WorkObject, etxSet *types.EtxSet) return nil, nil, nil, nil, 0, fmt.Errorf("invalid external transaction: etx %x is not in order or not found in unspent etx set", tx.Hash()) } if tx.To().IsInQiLedgerScope() { - if tx.Value().Int64() > types.MaxDenomination { // sanity check - return nil, nil, nil, nil, 0, fmt.Errorf("etx %032x emits UTXO with value %d greater than max denomination", tx.Hash(), tx.Value().Int64()) - } - // There are no more checks to be made as the ETX is worked so add it to the set - if err := statedb.CreateUTXO(tx.OriginatingTxHash(), tx.ETXIndex(), types.NewUtxoEntry(types.NewTxOut(uint8(tx.Value().Int64()), tx.To().Bytes()))); err != nil { - return nil, nil, nil, nil, 0, fmt.Errorf("could not create UTXO for etx %032x: %w", tx.Hash(), err) + if tx.ETXSender().Location().Equal(*tx.To().Location()) { // Quai->Qi Conversion + lock := new(big.Int).Add(header.Number(nodeCtx), big.NewInt(params.ConversionLockPeriod)) + primeTerminus := p.hc.GetHeaderByHash(header.PrimeTerminus()) + if primeTerminus == nil { + return nil, nil, nil, nil, 0, fmt.Errorf("could not find prime terminus header %032x", header.PrimeTerminus()) + } + value := misc.QuaiToQi(primeTerminus, tx.Value()) // convert Quai to Qi + txGas := tx.Gas() + 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 := uint8(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 + break + } + txGas -= params.CallValueTransferGas + if err := gp.SubGas(params.CallValueTransferGas); err != nil { + return nil, nil, nil, nil, 0, 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 + // the ETX hash is guaranteed to be unique + if err := statedb.CreateUTXO(tx.Hash(), outputIndex, types.NewUtxoEntry(types.NewTxOut(uint8(denomination), tx.To().Bytes(), lock))); err != nil { + return nil, nil, nil, nil, 0, err + } + log.Global.Infof("Converting Quai to Qi %032x with denomination %d index %d lock %d", tx.Hash(), denomination, outputIndex, lock) + outputIndex++ + } + } + } else { + // There are no more checks to be made as the ETX is worked so add it to the set + if err := statedb.CreateUTXO(tx.OriginatingTxHash(), tx.ETXIndex(), types.NewUtxoEntry(types.NewTxOut(uint8(tx.Value().Uint64()), tx.To().Bytes(), big.NewInt(0)))); err != nil { + return nil, nil, nil, nil, 0, err + } + // This Qi ETX should cost more gas + if err := gp.SubGas(params.CallValueTransferGas); err != nil { + return nil, nil, nil, nil, 0, 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 } - if err := gp.SubGas(params.CallValueTransferGas); err != nil { - return nil, nil, nil, nil, 0, 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 timeEtxDelta := time.Since(startTimeEtx) timeEtx += timeEtxDelta continue } else { - prevZeroBal := prepareApplyETX(statedb, tx, nodeLocation) + if tx.ETXSender().Location().Equal(*tx.To().Location()) { // Qi->Quai Conversion + msg.SetLock(new(big.Int).Add(header.Number(nodeCtx), big.NewInt(params.ConversionLockPeriod))) + primeTerminus := p.hc.GetHeaderByHash(header.PrimeTerminus()) + if primeTerminus == nil { + return nil, nil, nil, nil, 0, fmt.Errorf("could not find prime terminus header %032x", header.PrimeTerminus()) + } + // Convert Qi to Quai + msg.SetValue(misc.QiToQuai(primeTerminus, tx.Value())) + msg.SetData([]byte{}) // data is not used in conversion + log.Global.Infof("Converting Qi to Quai for ETX %032x with value %d lock %d", tx.Hash(), msg.Value().Uint64(), msg.Lock().Uint64()) + } + prevZeroBal := prepareApplyETX(statedb, msg.Value(), nodeLocation) receipt, err = applyTransaction(msg, parent, p.config, p.hc, nil, gp, statedb, blockNumber, blockHash, tx, usedGas, vmenv, &etxRLimit, &etxPLimit, p.logger) statedb.SetBalance(common.ZeroInternal(nodeLocation), prevZeroBal) // Reset the balance to what it previously was. Residual balance will be lost if err != nil { @@ -383,15 +431,18 @@ func (p *StateProcessor) Process(block *types.WorkObject, etxSet *types.EtxSet) totalCoinbaseOut := big.NewInt(0) for txOutIdx, txOut := range coinbaseTx.TxOut() { coinbase := common.BytesToAddress(txOut.Address, nodeLocation) - if !coinbase.IsInQiLedgerScope() { // a coinbase tx cannot emit an ETX + if !coinbase.IsInQiLedgerScope() { // a coinbase tx cannot emit a conversion return nil, nil, nil, nil, 0, fmt.Errorf("coinbase tx emits UTXO with To address not in the Qi ledger scope") } - if _, err := coinbase.InternalAddress(); err != nil { + if _, err := coinbase.InternalAddress(); err != nil { // a coinbase tx cannot emit an ETX return nil, nil, nil, nil, 0, fmt.Errorf("invalid coinbase address %v: %w", coinbase, err) } if !header.Coinbase().Equal(coinbase) { return nil, nil, nil, nil, 0, fmt.Errorf("coinbase tx emits UTXO with To address not equal to block coinbase") } + if txOutIdx > types.MaxOutputIndex { + return nil, nil, nil, nil, 0, fmt.Errorf("coinbase tx emits UTXO with index %d greater than max uint16", txOutIdx) + } utxo := types.NewUtxoEntry(&txOut) if err := statedb.CreateUTXO(coinbaseTx.Hash(), uint16(txOutIdx), utxo); err != nil { return nil, nil, nil, nil, 0, fmt.Errorf("could not create UTXO for coinbase tx %032x: %w", coinbaseTx.Hash(), err) @@ -462,7 +513,7 @@ func (p *StateProcessor) Process(block *types.WorkObject, etxSet *types.EtxSet) return receipts, qiEtxs, allLogs, statedb, *usedGas, nil } -func applyTransaction(msg types.Message, parent *types.WorkObject, config *params.ChainConfig, bc ChainContext, author *common.Address, gp *GasPool, statedb *state.StateDB, blockNumber *big.Int, blockHash common.Hash, tx *types.Transaction, usedGas *uint64, evm *vm.EVM, etxRLimit, etxPLimit *int, logger *log.Logger) (*types.Receipt, error) { +func applyTransaction(msg types.Message, parent *types.WorkObject, config *params.ChainConfig, bc ChainContext, author *common.Address, gp *types.GasPool, statedb *state.StateDB, blockNumber *big.Int, blockHash common.Hash, tx *types.Transaction, usedGas *uint64, evm *vm.EVM, etxRLimit, etxPLimit *int, logger *log.Logger) (*types.Receipt, error) { nodeLocation := config.Location // Create a new context to be used in the EVM environment. txContext := NewEVMTxContext(msg) @@ -526,7 +577,10 @@ func applyTransaction(msg types.Message, parent *types.WorkObject, config *param return receipt, err } -func ProcessQiTx(tx *types.Transaction, chain ChainContext, updateState bool, currentHeader *types.WorkObject, statedb *state.StateDB, gp *GasPool, usedGas *uint64, signer types.Signer, location common.Location, chainId big.Int, etxRLimit, etxPLimit *int) (*big.Int, []*types.Transaction, error) { +// ProcessQiTx processes a QiTx by spending the inputs and creating the outputs. +// Math is performed to verify the fee provided is sufficient to cover the gas cost. +// updateState is set to update the statedb in the case of the state processor, but not in the case of the txpool. +func ProcessQiTx(tx *types.Transaction, chain ChainContext, updateState bool, currentHeader *types.WorkObject, statedb *state.StateDB, gp *types.GasPool, usedGas *uint64, signer types.Signer, location common.Location, chainId big.Int, etxRLimit, etxPLimit *int) (*big.Int, []*types.ExternalTx, error) { // Sanity checks if tx == nil || tx.Type() != types.QiTxType { return nil, nil, fmt.Errorf("tx %032x is not a QiTx", tx.Hash()) @@ -537,14 +591,16 @@ func ProcessQiTx(tx *types.Transaction, chain ChainContext, updateState bool, cu if currentHeader == nil || statedb == nil || gp == nil || usedGas == nil || signer == nil || etxRLimit == nil || etxPLimit == nil { return nil, nil, errors.New("one of the parameters is nil") } - - addresses := make(map[common.AddressBytes]struct{}) - txGas := types.CalculateQiTxGas(tx) + *usedGas += txGas if err := gp.SubGas(txGas); err != nil { return nil, nil, err } - *usedGas += txGas + if *usedGas > currentHeader.GasLimit() { + return nil, nil, fmt.Errorf("tx %032x uses too much gas, have used %d out of %d", tx.Hash(), *usedGas, currentHeader.GasLimit()) + } + + addresses := make(map[common.AddressBytes]struct{}) totalQitIn := big.NewInt(0) pubKeys := make([]*btcec.PublicKey, 0) @@ -553,6 +609,9 @@ func ProcessQiTx(tx *types.Transaction, chain ChainContext, updateState bool, cu if utxo == nil { return nil, nil, fmt.Errorf("tx %032x spends non-existent UTXO %032x:%d", tx.Hash(), txIn.PreviousOutPoint.TxHash, txIn.PreviousOutPoint.Index) } + if utxo.Lock != nil && utxo.Lock.Cmp(currentHeader.Number(location.Context())) > 0 { + return nil, nil, fmt.Errorf("tx %032x spends locked UTXO %032x:%d locked until %s", tx.Hash(), txIn.PreviousOutPoint.TxHash, txIn.PreviousOutPoint.Index, utxo.Lock.String()) + } // Verify the pubkey address := crypto.PubkeyBytesToAddress(txIn.PubKey, location) entryAddr := common.BytesToAddress(utxo.Address, location) @@ -587,8 +646,11 @@ func ProcessQiTx(tx *types.Transaction, chain ChainContext, updateState bool, cu } var ETXRCount int var ETXPCount int - etxs := make([]*types.Transaction, 0) + etxs := make([]*types.ExternalTx, 0) totalQitOut := big.NewInt(0) + totalConvertQitOut := big.NewInt(0) + conversion := false + var convertAddress common.Address for txOutIdx, txOut := range tx.TxOut() { // It would be impossible for a tx to have this many outputs based on block gas limit, but cap it here anyways if txOutIdx > types.MaxOutputIndex { @@ -605,15 +667,24 @@ func ProcessQiTx(tx *types.Transaction, chain ChainContext, updateState bool, cu totalQitOut.Add(totalQitOut, types.Denominations[txOut.Denomination]) toAddr := common.BytesToAddress(txOut.Address, location) - if !toAddr.IsInQiLedgerScope() { - return nil, nil, fmt.Errorf("tx [%v] emits UTXO with To address not in the Qi ledger scope", tx.Hash().Hex()) - } + // Enforce no address reuse if _, exists := addresses[toAddr.Bytes20()]; exists { return nil, nil, errors.New("Duplicate address in QiTx outputs: " + toAddr.String()) } addresses[toAddr.Bytes20()] = struct{}{} + if toAddr.Location().Equal(location) && toAddr.IsInQuaiLedgerScope() { // Qi->Quai conversion + conversion = true + convertAddress = toAddr + if txOut.Denomination < params.MinQiConversionDenomination { + return nil, nil, fmt.Errorf("tx %v emits UTXO with value %d less than minimum denomination %d", tx.Hash().Hex(), txOut.Denomination, params.MinQiConversionDenomination) + } + totalConvertQitOut.Add(totalConvertQitOut, types.Denominations[txOut.Denomination]) // Add to total conversion output for aggregation + delete(addresses, toAddr.Bytes20()) + continue + } + if !toAddr.Location().Equal(location) { // This output creates an ETX // Cross-region? if toAddr.Location().CommonDom(location).Context() == common.REGION_CTX { @@ -629,17 +700,21 @@ func ProcessQiTx(tx *types.Transaction, chain ChainContext, updateState bool, cu if ETXPCount > *etxPLimit { return nil, nil, fmt.Errorf("tx [%v] emits too many cross-prime ETXs for block. emitted: %d, limit: %d", tx.Hash().Hex(), ETXPCount, etxPLimit) } - - // We should require some kind of extra fee here - etxInner := types.ExternalTx{Value: big.NewInt(int64(txOut.Denomination)), To: &toAddr, Sender: common.ZeroAddress(location), OriginatingTxHash: tx.Hash(), ETXIndex: uint16(txOutIdx), Gas: params.TxGas} - etx := types.NewTx(&etxInner) primeTerminus := currentHeader.PrimeTerminus() primeTerminusHeader := chain.GetHeaderByHash(primeTerminus) + if primeTerminusHeader == nil { + return nil, nil, fmt.Errorf("could not find prime terminus header %032x", primeTerminus) + } + if !toAddr.IsInQiLedgerScope() { + return nil, nil, fmt.Errorf("tx [%v] emits UTXO with To address not in the Qi ledger scope", tx.Hash().Hex()) + } if !chain.CheckIfEtxIsEligible(primeTerminusHeader.EtxEligibleSlices(), *toAddr.Location()) { return nil, nil, fmt.Errorf("etx emitted by tx [%v] going to a slice that is not eligible to receive etx %v", tx.Hash().Hex(), *toAddr.Location()) } - etxs = append(etxs, etx) - log.Global.Debug("Added UTXO ETX to ETX list") + + // We should require some kind of extra fee here + etxInner := types.ExternalTx{Value: big.NewInt(int64(txOut.Denomination)), To: &toAddr, Sender: common.ZeroAddress(location), OriginatingTxHash: tx.Hash(), ETXIndex: uint16(txOutIdx), Gas: params.TxGas} + etxs = append(etxs, &etxInner) } else { // This output creates a normal UTXO utxo := types.NewUtxoEntry(&txOut) @@ -679,13 +754,35 @@ func ProcessQiTx(tx *types.Transaction, chain ChainContext, updateState bool, cu // the fee to pay the basefee/miner is the difference between inputs and outputs txFeeInQit := new(big.Int).Sub(totalQitIn, totalQitOut) // Check tx against required base fee and gas - baseFeeInQi := misc.QuaiToQi(currentHeader, currentHeader.BaseFee()) - minimumFee := new(big.Int).Mul(baseFeeInQi, big.NewInt(int64(txGas))) + requiredGas := txGas + (uint64(len(etxs)) * params.TxGas) // Each ETX costs extra gas that is paid in the origin + if requiredGas < txGas { + // Overflow + return nil, nil, fmt.Errorf("tx %032x has too many ETXs to calculate required gas", tx.Hash()) + } + minimumFeeInQuai := new(big.Int).Mul(big.NewInt(int64(requiredGas)), currentHeader.BaseFee()) + minimumFee := misc.QuaiToQi(currentHeader, minimumFeeInQuai) if txFeeInQit.Cmp(minimumFee) < 0 { - return nil, nil, fmt.Errorf("tx %032x has insufficient fee for base fee of %d and gas of %d", tx.Hash(), baseFeeInQi.Uint64(), txGas) + return nil, nil, fmt.Errorf("tx %032x has insufficient fee for base fee, have %d want %d", tx.Hash(), txFeeInQit.Uint64(), minimumFee.Uint64()) } - // Miner gets remainder of fee after base fee + // Miner gets remainder of fee after base fee, except in the convert case txFeeInQit.Sub(txFeeInQit, minimumFee) + if conversion { + // Since this transaction contains a conversion, the rest of the tx gas is given to conversion + remainingTxFeeInQuai := misc.QiToQuai(currentHeader, txFeeInQit) + // Fee is basefee * gas, so gas remaining is fee remaining / basefee + remainingGas := new(big.Int).Div(remainingTxFeeInQuai, currentHeader.BaseFee()) + if remainingGas.Uint64() > (currentHeader.GasLimit() / params.MinimumEtxGasDivisor) { + // Limit ETX gas to max ETX gas limit (the rest is burned) + remainingGas = new(big.Int).SetUint64(currentHeader.GasLimit() / params.MinimumEtxGasDivisor) + } + ETXPCount++ + if ETXPCount > *etxPLimit { + return nil, nil, fmt.Errorf("tx [%v] emits too many cross-prime ETXs for block. emitted: %d, limit: %d", tx.Hash().Hex(), ETXPCount, etxPLimit) + } + etxInner := types.ExternalTx{Value: totalConvertQitOut, To: &convertAddress, Sender: common.ZeroAddress(location), OriginatingTxHash: tx.Hash(), Gas: remainingGas.Uint64()} // Value is in Qits not Denomination + etxs = append(etxs, &etxInner) + txFeeInQit.Sub(txFeeInQit, txFeeInQit) // Fee goes entirely to gas to pay for conversion + } *etxRLimit -= ETXRCount *etxPLimit -= ETXPCount @@ -791,17 +888,27 @@ func (p *StateProcessor) Apply(batch ethdb.Batch, block *types.WorkObject, newIn // 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, bc ChainContext, author *common.Address, gp *GasPool, statedb *state.StateDB, header *types.WorkObject, tx *types.Transaction, usedGas *uint64, cfg vm.Config, etxRLimit, etxPLimit *int, logger *log.Logger) (*types.Receipt, error) { +func ApplyTransaction(config *params.ChainConfig, parent *types.WorkObject, bc ChainContext, author *common.Address, gp *types.GasPool, statedb *state.StateDB, header *types.WorkObject, tx *types.Transaction, usedGas *uint64, cfg vm.Config, etxRLimit, etxPLimit *int, logger *log.Logger) (*types.Receipt, error) { nodeCtx := config.Location.Context() msg, err := tx.AsMessage(types.MakeSigner(config, header.Number(nodeCtx)), header.BaseFee()) if err != nil { return nil, err } + if tx.Type() == types.ExternalTxType && tx.ETXSender().Location().Equal(*tx.To().Location()) { // Qi->Quai Conversion + msg.SetLock(new(big.Int).Add(header.Number(nodeCtx), big.NewInt(params.ConversionLockPeriod))) + primeTerminus := bc.GetHeaderByHash(header.PrimeTerminus()) + if primeTerminus == nil { + return nil, fmt.Errorf("could not find prime terminus header %032x", header.PrimeTerminus()) + } + // Convert Qi to Quai + msg.SetValue(misc.QiToQuai(primeTerminus, tx.Value())) + msg.SetData([]byte{}) // data is not used in conversion + } // Create a new context to be used in the EVM environment blockContext := NewEVMBlockContext(header, bc, author) vmenv := vm.NewEVM(blockContext, vm.TxContext{}, statedb, config, cfg) if tx.Type() == types.ExternalTxType { - prevZeroBal := prepareApplyETX(statedb, tx, config.Location) + prevZeroBal := prepareApplyETX(statedb, msg.Value(), config.Location) receipt, err := applyTransaction(msg, parent, config, bc, author, gp, statedb, header.Number(nodeCtx), header.Hash(), tx, usedGas, vmenv, etxRLimit, etxPLimit, logger) statedb.SetBalance(common.ZeroInternal(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, err @@ -1094,7 +1201,7 @@ func (p *StateProcessor) StateAtTransaction(block *types.WorkObject, txIndex int // Not yet the searched for transaction, execute on top of the current state vmenv := vm.NewEVM(context, txContext, statedb, p.hc.Config(), vm.Config{}) statedb.Prepare(tx.Hash(), idx) - if _, err := ApplyMessage(vmenv, msg, new(GasPool).AddGas(tx.Gas())); err != nil { + 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) } // Ensure any modifications are committed to the state @@ -1118,12 +1225,9 @@ func (p *StateProcessor) Stop() { p.logger.Info("State Processor stopped") } -func prepareApplyETX(statedb *state.StateDB, tx *types.Transaction, nodeLocation common.Location) *big.Int { +func prepareApplyETX(statedb *state.StateDB, value *big.Int, nodeLocation common.Location) *big.Int { prevZeroBal := statedb.GetBalance(common.ZeroInternal(nodeLocation)) // Get current zero address balance - fee := big.NewInt(0).Add(tx.GasFeeCap(), tx.GasTipCap()) // Add gas price cap to miner tip cap - fee.Mul(fee, big.NewInt(int64(tx.Gas()))) // Multiply gas price by gas limit (may need to check for int64 overflow) - total := big.NewInt(0).Add(fee, tx.Value()) // Add gas fee to value - statedb.SetBalance(common.ZeroInternal(nodeLocation), total) // Use zero address at temp placeholder and set it to gas fee plus value + statedb.SetBalance(common.ZeroInternal(nodeLocation), value) // Use zero address at temp placeholder and set it to value return prevZeroBal } diff --git a/core/state_transition.go b/core/state_transition.go index c4612e9687..2e38c57d13 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -52,7 +52,7 @@ The state transitioning model does all the necessary work to work out a valid ne 6) Derive new state root */ type StateTransition struct { - gp *GasPool + gp *types.GasPool msg Message gas uint64 gasPrice *big.Int @@ -83,6 +83,7 @@ type Message interface { ETXSender() common.Address Type() byte Hash() common.Hash + Lock() *big.Int } // ExecutionResult includes all output after executing given evm @@ -161,7 +162,7 @@ func IntrinsicGas(data []byte, accessList types.AccessList, isContractCreation b } // NewStateTransition initialises and returns a new state transition object. -func NewStateTransition(evm *vm.EVM, msg Message, gp *GasPool) *StateTransition { +func NewStateTransition(evm *vm.EVM, msg Message, gp *types.GasPool) *StateTransition { return &StateTransition{ gp: gp, evm: evm, @@ -182,7 +183,7 @@ func NewStateTransition(evm *vm.EVM, msg Message, gp *GasPool) *StateTransition // the gas used (which includes gas refunds) and an error if it failed. An error always // indicates a core error meaning that the message would always fail for that particular // state and would never be accepted within a block. -func ApplyMessage(evm *vm.EVM, msg Message, gp *GasPool) (*ExecutionResult, error) { +func ApplyMessage(evm *vm.EVM, msg Message, gp *types.GasPool) (*ExecutionResult, error) { return NewStateTransition(evm, msg, gp).TransitionDb() } @@ -372,7 +373,7 @@ func (st *StateTransition) TransitionDb() (*ExecutionResult, error) { return nil, err } st.state.SetNonce(from, st.state.GetNonce(addr)+1) - ret, st.gas, vmerr = st.evm.Call(sender, st.to(), st.data, st.gas, st.value) + ret, st.gas, vmerr = st.evm.Call(sender, st.to(), st.data, st.gas, st.value, st.msg.Lock()) } // At this point, the execution completed, so the ETX cache can be dumped and reset @@ -391,8 +392,8 @@ func (st *StateTransition) TransitionDb() (*ExecutionResult, error) { if err != nil { return nil, err } - if coinbase.IsInQuaiLedgerScope() { - st.state.AddBalance(coinbase, new(big.Int).Mul(new(big.Int).SetUint64(st.gasUsed()), effectiveTip)) // todo: etxs no longer pay the miner a fee + if coinbase.IsInQuaiLedgerScope() && !st.msg.IsETX() { + st.state.AddBalance(coinbase, new(big.Int).Mul(new(big.Int).SetUint64(st.gasUsed()), effectiveTip)) } return &ExecutionResult{ diff --git a/core/tx_pool.go b/core/tx_pool.go index 3e702641e8..1b46b24f45 100644 --- a/core/tx_pool.go +++ b/core/tx_pool.go @@ -1053,11 +1053,6 @@ func (pool *TxPool) addTxs(txs []*types.Transaction, local, sync bool) []error { } continue } - if tx.To() != nil && tx.To().IsInQiLedgerScope() { - errs[i] = common.MakeErrQiAddress(tx.To().Hex()) - invalidTxMeter.Add(1) - continue - } // Exclude transactions with invalid signatures as soon as // possible and cache senders in transactions before // obtaining lock @@ -1128,7 +1123,7 @@ func (pool *TxPool) addQiTx(tx *types.Transaction, grabLock bool) error { location := pool.chainconfig.Location currentBlock := pool.chain.CurrentBlock() - gp := GasPool(currentBlock.GasLimit()) + gp := types.GasPool(currentBlock.GasLimit()) etxRLimit := len(currentBlock.Transactions()) / params.ETXRegionMaxFraction if etxRLimit < params.ETXRLimitMin { etxRLimit = params.ETXRLimitMin @@ -1142,11 +1137,13 @@ func (pool *TxPool) addQiTx(tx *types.Transaction, grabLock bool) error { } fee, _, err := ProcessQiTx(tx, pool.chain, false, pool.chain.CurrentBlock(), pool.currentState, &gp, new(uint64), pool.signer, location, *pool.chainconfig.ChainID, &etxRLimit, &etxPLimit) if err != nil { - pool.mu.RUnlock() + if grabLock { + pool.mu.RUnlock() + } pool.logger.WithFields(logrus.Fields{ "tx": tx.Hash().String(), "err": err, - }).Error("Invalid qi tx") + }).Error("Invalid Qi transaction") return err } if grabLock { diff --git a/core/gaspool.go b/core/types/gaspool.go similarity index 96% rename from core/gaspool.go rename to core/types/gaspool.go index e3795c1ee9..3ab99b7323 100644 --- a/core/gaspool.go +++ b/core/types/gaspool.go @@ -14,9 +14,10 @@ // You should have received a copy of the GNU Lesser General Public License // along with the go-ethereum library. If not, see . -package core +package types import ( + "errors" "fmt" "math" ) @@ -38,7 +39,7 @@ func (gp *GasPool) AddGas(amount uint64) *GasPool { // available and returns an error otherwise. func (gp *GasPool) SubGas(amount uint64) error { if uint64(*gp) < amount { - return ErrGasLimitReached + return errors.New("gas limit reached") } *(*uint64)(gp) -= amount return nil diff --git a/core/types/proto_block.pb.go b/core/types/proto_block.pb.go index 94133d4326..7e67804a6d 100644 --- a/core/types/proto_block.pb.go +++ b/core/types/proto_block.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.30.0 -// protoc v4.25.1 +// protoc-gen-go v1.32.0 +// protoc v4.25.2 // source: core/types/proto_block.proto package types @@ -1819,6 +1819,7 @@ type ProtoTxOut struct { Denomination *uint32 `protobuf:"varint,1,opt,name=denomination,proto3,oneof" json:"denomination,omitempty"` Address []byte `protobuf:"bytes,2,opt,name=address,proto3,oneof" json:"address,omitempty"` + Lock []byte `protobuf:"bytes,3,opt,name=lock,proto3,oneof" json:"lock,omitempty"` } func (x *ProtoTxOut) Reset() { @@ -1867,6 +1868,13 @@ func (x *ProtoTxOut) GetAddress() []byte { return nil } +func (x *ProtoTxOut) GetLock() []byte { + if x != nil { + return x.Lock + } + return nil +} + var File_core_types_proto_block_proto protoreflect.FileDescriptor var file_core_types_proto_block_proto_rawDesc = []byte{ @@ -2258,18 +2266,20 @@ var file_core_types_proto_block_proto_rawDesc = []byte{ 0x48, 0x00, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x88, 0x01, 0x01, 0x12, 0x19, 0x0a, 0x05, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x01, 0x52, 0x05, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x88, 0x01, 0x01, 0x42, 0x07, 0x0a, 0x05, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x42, - 0x08, 0x0a, 0x06, 0x5f, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x22, 0x71, 0x0a, 0x0a, 0x50, 0x72, 0x6f, - 0x74, 0x6f, 0x54, 0x78, 0x4f, 0x75, 0x74, 0x12, 0x27, 0x0a, 0x0c, 0x64, 0x65, 0x6e, 0x6f, 0x6d, - 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x00, 0x52, - 0x0c, 0x64, 0x65, 0x6e, 0x6f, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x88, 0x01, 0x01, - 0x12, 0x1d, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0c, 0x48, 0x01, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x88, 0x01, 0x01, 0x42, - 0x0f, 0x0a, 0x0d, 0x5f, 0x64, 0x65, 0x6e, 0x6f, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x42, 0x33, 0x5a, 0x31, - 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x64, 0x6f, 0x6d, 0x69, 0x6e, - 0x61, 0x6e, 0x74, 0x2d, 0x73, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x69, 0x65, 0x73, 0x2f, 0x67, - 0x6f, 0x2d, 0x71, 0x75, 0x61, 0x69, 0x2f, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x74, 0x79, 0x70, 0x65, - 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x08, 0x0a, 0x06, 0x5f, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x22, 0x93, 0x01, 0x0a, 0x0a, 0x50, 0x72, + 0x6f, 0x74, 0x6f, 0x54, 0x78, 0x4f, 0x75, 0x74, 0x12, 0x27, 0x0a, 0x0c, 0x64, 0x65, 0x6e, 0x6f, + 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x48, 0x00, + 0x52, 0x0c, 0x64, 0x65, 0x6e, 0x6f, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x88, 0x01, + 0x01, 0x12, 0x1d, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0c, 0x48, 0x01, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x88, 0x01, 0x01, + 0x12, 0x17, 0x0a, 0x04, 0x6c, 0x6f, 0x63, 0x6b, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x48, 0x02, + 0x52, 0x04, 0x6c, 0x6f, 0x63, 0x6b, 0x88, 0x01, 0x01, 0x42, 0x0f, 0x0a, 0x0d, 0x5f, 0x64, 0x65, + 0x6e, 0x6f, 0x6d, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x61, + 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x42, 0x07, 0x0a, 0x05, 0x5f, 0x6c, 0x6f, 0x63, 0x6b, 0x42, + 0x33, 0x5a, 0x31, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x64, 0x6f, + 0x6d, 0x69, 0x6e, 0x61, 0x6e, 0x74, 0x2d, 0x73, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x69, 0x65, + 0x73, 0x2f, 0x67, 0x6f, 0x2d, 0x71, 0x75, 0x61, 0x69, 0x2f, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x74, + 0x79, 0x70, 0x65, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/core/types/proto_block.proto b/core/types/proto_block.proto index 0c29643573..430b72f2e5 100644 --- a/core/types/proto_block.proto +++ b/core/types/proto_block.proto @@ -167,4 +167,5 @@ message ProtoOutPoint { message ProtoTxOut { optional uint32 denomination = 1; optional bytes address = 2; + optional bytes lock = 3; } diff --git a/core/types/qi_tx.go b/core/types/qi_tx.go index 2136c33654..4370b837cb 100644 --- a/core/types/qi_tx.go +++ b/core/types/qi_tx.go @@ -121,6 +121,7 @@ func CalculateQiTxGas(transaction *Transaction) uint64 { if transaction.Type() != QiTxType { panic("CalculateQiTxGas called on a transaction that is not a Qi transaction") } + // TODO: This should check for ETXs (and conversion) and calculate expected gas for those as well return uint64(len(transaction.TxIn()))*params.SloadGas + uint64(len(transaction.TxOut()))*params.CallValueTransferGas + params.EcrecoverGas } diff --git a/core/types/transaction.go b/core/types/transaction.go index a6515ba5a2..2d1dc74ecc 100644 --- a/core/types/transaction.go +++ b/core/types/transaction.go @@ -29,6 +29,7 @@ import ( "github.com/dominant-strategies/go-quai/common" "github.com/dominant-strategies/go-quai/common/math" "github.com/dominant-strategies/go-quai/log" + "github.com/dominant-strategies/go-quai/params" "google.golang.org/protobuf/proto" "github.com/dominant-strategies/go-quai/crypto" @@ -546,7 +547,7 @@ func (tx *Transaction) To() *common.Address { return &cpy } -func (tx *Transaction) SetTo(addr common.Address) { +func (tx *Transaction) SetGas(addr common.Address) { tx.inner.setTo(addr) } @@ -709,6 +710,10 @@ func (tx *Transaction) ConfirmationCtx(nodeLocation common.Location) int { if ctx := tx.confirmCtx.Load(); ctx != nil { return ctx.(int) } + if tx.ETXSender().Location().Equal(*tx.To().Location()) { + // If the ETX sender and the destination chain are the same, the ETX is a conversion tx + return params.ConversionConfirmationContext + } ctx := tx.To().Location().CommonDom(tx.FromChain(nodeLocation)).Context() tx.confirmCtx.Store(ctx) @@ -1018,6 +1023,7 @@ type Message struct { etxsender common.Address // only used in ETX txtype byte hash common.Hash + lock *big.Int } func NewMessage(from common.Address, to *common.Address, nonce uint64, amount *big.Int, gasLimit uint64, gasPrice, gasFeeCap, gasTipCap *big.Int, data []byte, accessList AccessList, isETX bool) Message { @@ -1082,6 +1088,7 @@ func (tx *Transaction) AsMessageWithSender(s Signer, baseFee *big.Int, sender *c isETX: false, txtype: tx.Type(), hash: tx.Hash(), + lock: new(big.Int), } // If baseFee provided, set gasPrice to effectiveGasPrice. if baseFee != nil { @@ -1117,6 +1124,19 @@ func (m Message) IsETX() bool { return m.isETX } func (m Message) ETXSender() common.Address { return m.etxsender } func (m Message) Type() byte { return m.txtype } func (m Message) Hash() common.Hash { return m.hash } +func (m Message) Lock() *big.Int { return m.lock } + +func (m *Message) SetValue(v *big.Int) { + m.amount = v +} + +func (m *Message) SetLock(lock *big.Int) { + m.lock = lock +} + +func (m *Message) SetData(data []byte) { + m.data = data +} // AccessList is an access list. type AccessList []AccessTuple diff --git a/core/types/utxo.go b/core/types/utxo.go index 3a03d0b5ed..befd810c63 100644 --- a/core/types/utxo.go +++ b/core/types/utxo.go @@ -165,6 +165,7 @@ func (txOuts *TxOuts) ProtoDecode(protoTxOuts *ProtoTxOuts) error { type TxOut struct { Denomination uint8 Address []byte + Lock *big.Int // Block height the entry unlocks. 0 or nil = unlocked } func (txOut TxOut) ProtoEncode() (*ProtoTxOut, error) { @@ -173,6 +174,11 @@ func (txOut TxOut) ProtoEncode() (*ProtoTxOut, error) { denomination := uint32(txOut.Denomination) protoTxOut.Denomination = &denomination protoTxOut.Address = txOut.Address + if txOut.Lock == nil { + protoTxOut.Lock = big.NewInt(0).Bytes() + } else { + protoTxOut.Lock = txOut.Lock.Bytes() + } return protoTxOut, nil } @@ -183,14 +189,48 @@ func (txOut *TxOut) ProtoDecode(protoTxOut *ProtoTxOut) error { } txOut.Denomination = uint8(protoTxOut.GetDenomination()) txOut.Address = protoTxOut.Address + txOut.Lock = new(big.Int).SetBytes(protoTxOut.Lock) return nil } // NewTxOut returns a new Qi transaction output with the provided // transaction value and address. -func NewTxOut(denomination uint8, address []byte) *TxOut { +func NewTxOut(denomination uint8, address []byte, lock *big.Int) *TxOut { return &TxOut{ Denomination: denomination, Address: address, + Lock: lock, + } +} + +// UtxoEntry houses details about an individual transaction output in a utxo +// view such as whether or not it was contained in a coinbase tx, the height of +// the block that contains the tx, whether or not it is spent, its public key +// script, and how much it pays. +type UtxoEntry struct { + Denomination uint8 + Address []byte // The address of the output holder. + Lock *big.Int // Block height the entry unlocks. 0 = unlocked +} + +// Clone returns a shallow copy of the utxo entry. +func (entry *UtxoEntry) Clone() *UtxoEntry { + if entry == nil { + return nil + } + + return &UtxoEntry{ + Denomination: entry.Denomination, + Address: entry.Address, + Lock: new(big.Int).Set(entry.Lock), + } +} + +// NewUtxoEntry returns a new UtxoEntry built from the arguments. +func NewUtxoEntry(txOut *TxOut) *UtxoEntry { + return &UtxoEntry{ + Denomination: txOut.Denomination, + Address: txOut.Address, + Lock: txOut.Lock, } } diff --git a/core/types/utxo_viewpoint.go b/core/types/utxo_viewpoint.go deleted file mode 100644 index 5e9a79fe18..0000000000 --- a/core/types/utxo_viewpoint.go +++ /dev/null @@ -1,30 +0,0 @@ -package types - -// UtxoEntry houses details about an individual transaction output in a utxo -// view such as whether or not it was contained in a coinbase tx, the height of -// the block that contains the tx, whether or not it is spent, its public key -// script, and how much it pays. -type UtxoEntry struct { - Denomination uint8 - Address []byte // The address of the output holder. -} - -// Clone returns a shallow copy of the utxo entry. -func (entry *UtxoEntry) Clone() *UtxoEntry { - if entry == nil { - return nil - } - - return &UtxoEntry{ - Denomination: entry.Denomination, - Address: entry.Address, - } -} - -// NewUtxoEntry returns a new UtxoEntry built from the arguments. -func NewUtxoEntry(txOut *TxOut) *UtxoEntry { - return &UtxoEntry{ - Denomination: txOut.Denomination, - Address: txOut.Address, - } -} diff --git a/core/types/wo.go b/core/types/wo.go index 4025521d8e..d0e972619b 100644 --- a/core/types/wo.go +++ b/core/types/wo.go @@ -323,8 +323,8 @@ func (wo *WorkObject) QiTransactionsWithoutCoinbase() []*Transaction { func (wo *WorkObject) QuaiTransactionsWithoutCoinbase() []*Transaction { quaiTxs := make([]*Transaction, 0) for i, t := range wo.Transactions() { - if i == 0 && IsCoinBaseTx(t, wo.woHeader.parentHash, wo.woHeader.location) { - // ignore the Quai coinbase tx to comply with prior functionality as it is not a normal transaction + if i == 0 && IsCoinBaseTx(t, wo.woHeader.parentHash, wo.woHeader.location) || t.Type() == QiTxType || (t.Type() == ExternalTxType && t.ETXSender().Location().Equal(*t.To().Location())) { + // ignore the Quai coinbase tx and Quai->Qi to comply with prior functionality as it is not a normal transaction continue } if t.Type() != QiTxType { diff --git a/core/vm/contracts.go b/core/vm/contracts.go index bdc09d1b65..a685bc8192 100644 --- a/core/vm/contracts.go +++ b/core/vm/contracts.go @@ -25,6 +25,7 @@ import ( "github.com/dominant-strategies/go-quai/common" "github.com/dominant-strategies/go-quai/common/math" + "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" @@ -43,8 +44,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) { @@ -57,6 +59,7 @@ func InitializePrecompiles(nodeLocation common.Location) { PrecompiledContracts[common.HexToAddressBytes(fmt.Sprintf("0x%x00000000000000000000000000000000000007", nodeLocation.BytePrefix()))] = &bn256ScalarMul{} PrecompiledContracts[common.HexToAddressBytes(fmt.Sprintf("0x%x00000000000000000000000000000000000008", nodeLocation.BytePrefix()))] = &bn256Pairing{} PrecompiledContracts[common.HexToAddressBytes(fmt.Sprintf("0x%x00000000000000000000000000000000000009", nodeLocation.BytePrefix()))] = &blake2F{} + LockupContractAddresses[[2]byte{nodeLocation[0], nodeLocation[1]}] = common.HexToAddress(fmt.Sprintf("0x%x0000000000000000000000000000000000000A", nodeLocation.BytePrefix()), nodeLocation) } // ActivePrecompiles returns the precompiles enabled with the current configuration. @@ -486,3 +489,171 @@ func intToByteArray20(n uint8) [20]byte { byteArray[19] = byte(n) // Use the last byte for the integer return byteArray } + +func RequiredGas(input []byte) uint64 { + return 0 +} + +// RedeemQuai executes the lockup contract to redeem the locked balance(s) for the sender +func RedeemQuai(statedb StateDB, sender common.Address, gas *types.GasPool, blockHeight *big.Int, lockupContractAddress common.Address) (uint64, error) { + internalContractAddress, err := lockupContractAddress.InternalAndQuaiAddress() + if err != nil { + return 0, err + } + // The current lock is the next available lock to redeem (in order of creation) + currentLockHash := statedb.GetState(internalContractAddress, sender.Hash()) + gasUsed := params.ColdSloadCost + if gas.SubGas(params.ColdSloadCost) != nil { + // This contract does not revert. If the caller runs out of gas, we just stop + return gasUsed, ErrOutOfGas + } + if (currentLockHash == common.Hash{}) { + return gasUsed, errors.New("lockup not found") + } + currentLockNumber := new(big.Int).SetBytes(currentLockHash[:]) + if !currentLockNumber.IsUint64() { + return gasUsed, errors.New("account has locked too many times, overflows uint64") + } + + for i := int64(0); i < math.MaxInt64; i++ { // TODO: We should decide on a reasonable limit + // Ensure we have enough gas to complete this step entirely + requiredGas := params.ColdSloadCost + params.SstoreResetGas + params.ColdSloadCost + params.SstoreResetGas + params.SstoreResetGas + params.CallValueTransferGas + if gas.Gas() < requiredGas { + return gasUsed, fmt.Errorf("insufficient gas to complete lockup redemption, required %d, have %d", requiredGas, gas.Gas()) + } + // The key is zero padded + sender's address + current lock pointer + 1 + key := sender.Bytes() + // Append current lock pointer to the key + key = binary.BigEndian.AppendUint64(key, currentLockNumber.Uint64()) + key = append(key, byte(1)) // Set the 29th byte of the key to 1 to get lock height + if len(key) > common.HashLength { + return gasUsed, errors.New("lockup key is too long, math is broken") + } + lockHash := statedb.GetState(internalContractAddress, common.BytesToHash(key)) + gasUsed += params.ColdSloadCost + if gas.SubGas(params.ColdSloadCost) != nil { + // This contract does not revert. If the caller runs out of gas, we just stop + return gasUsed, ErrOutOfGas + } + if (lockHash == common.Hash{}) { + // Lock doesn't exist, so we're done + return gasUsed, nil + } + lock := new(big.Int).SetBytes(lockHash[:]) + if lock.Cmp(blockHeight) > 0 { + // lock not ready yet. Lockups are stored in FIFO order, so we don't have to go through the rest + return gasUsed, fmt.Errorf("lockup not ready yet, lock height: %d, current block height: %d", lock, blockHeight) + } + // Set the lock to zero + statedb.SetState(internalContractAddress, common.BytesToHash(key), common.Hash{}) + gasUsed += params.SstoreResetGas + if gas.SubGas(params.SstoreResetGas) != nil { + // This contract does not revert. If the caller runs out of gas, we just stop + return gasUsed, ErrOutOfGas + } + key[28] = 0 // Set the 29th byte of the key to 0 for balance + balanceHash := statedb.GetState(internalContractAddress, common.BytesToHash(key)) + gasUsed += params.ColdSloadCost + if gas.SubGas(params.ColdSloadCost) != nil { + return gasUsed, ErrOutOfGas + } + if (balanceHash == common.Hash{}) { + // If locked balance after covnert is zero, either it doesn't exist or something is broken + return gasUsed, errors.New("balance not found") + } + // Set the locked balance to zero + statedb.SetState(internalContractAddress, common.BytesToHash(key), common.Hash{}) + gasUsed += params.SstoreResetGas + if gas.SubGas(params.SstoreResetGas) != nil { + return gasUsed, ErrOutOfGas + } + // Increment the current lock counter + currentLockNumber.Add(currentLockNumber, big1) + currentLockHash = common.BytesToHash(binary.BigEndian.AppendUint64([]byte{}, currentLockNumber.Uint64())) + statedb.SetState(internalContractAddress, sender.Hash(), currentLockHash) + gasUsed += params.SstoreResetGas + if gas.SubGas(params.SstoreResetGas) != nil { + return gasUsed, ErrOutOfGas + } + + // Redeem the balance for the sender + balance := new(big.Int).SetBytes(balanceHash[:]) + internal, err := sender.InternalAndQuaiAddress() + if err != nil { + return gasUsed, err + } + statedb.AddBalance(internal, balance) + gasUsed += params.CallValueTransferGas + if gas.SubGas(params.CallValueTransferGas) != nil { + return gasUsed, ErrOutOfGas + } + } + + return gasUsed, errors.New("account has locked too many times, overflows int64") +} + +// AddNewLock adds a new locked balance to the lockup contract +func AddNewLock(statedb StateDB, toAddr common.Address, gas *types.GasPool, lock *big.Int, balance *big.Int, lockupContractAddress common.Address) (uint64, error) { + internalContractAddress, err := lockupContractAddress.InternalAndQuaiAddress() + if err != nil { + return 0, err + } + if len(lock.Bytes()) > common.HashLength || len(balance.Bytes()) > common.HashLength { + return 0, errors.New("lock or balance is too large") + } + currentLockHash := statedb.GetState(internalContractAddress, common.BytesToHash(toAddr.Bytes())) + gasUsed := params.ColdSloadCost + if gas.SubGas(params.ColdSloadCost) != nil { + // This contract does not revert. If the caller runs out of gas, we just stop + return gasUsed, ErrOutOfGas + } + if (currentLockHash == common.Hash{}) { + // No lock found, create a new one + statedb.SetState(internalContractAddress, common.BytesToHash(toAddr.Bytes()), common.BytesToHash([]byte{1})) + currentLockHash = common.BytesToHash([]byte{1}) + } + currentLockNumber := new(big.Int).SetBytes(currentLockHash[:]) + if !currentLockNumber.IsUint64() { + return gasUsed, errors.New("account has locked too many times, overflows uint64") + } + + for i := int64(0); i < math.MaxInt64; i++ { // TODO: We should decide on a reasonable limit + // Ensure we have enough gas to complete this step entirely + requiredGas := params.ColdSloadCost + params.SstoreSetGas + params.SstoreSetGas + if gas.Gas() < requiredGas { + return gasUsed, fmt.Errorf("insufficient gas to add new lock, required %d, got %d", requiredGas, gas.Gas()) + } + key := toAddr.Bytes() + // Append current lock to the key + key = binary.BigEndian.AppendUint64(key, currentLockNumber.Uint64()) + key = append(key, byte(1)) // Set the 29th byte of the key to 1 for lockup + if len(key) > common.HashLength { + return gasUsed, errors.New("lockup key is too long, math is broken") + } + lockHash := statedb.GetState(internalContractAddress, common.BytesToHash(key)) + gasUsed += params.ColdSloadCost + if gas.SubGas(params.ColdSloadCost) != nil { + // This contract does not revert. If the caller runs out of gas, we just stop + return gasUsed, ErrOutOfGas + } + if (lockHash == common.Hash{}) { + // Lock doesn't exist, so add the new one here + statedb.SetState(internalContractAddress, common.BytesToHash(key), common.BytesToHash(lock.Bytes())) + gasUsed += params.SstoreSetGas + if gas.SubGas(params.SstoreSetGas) != nil { + return gasUsed, ErrOutOfGas + } + key[28] = 0 // Set the 29th byte of the key to 0 for balance + statedb.SetState(internalContractAddress, common.BytesToHash(key), common.BytesToHash(balance.Bytes())) + gasUsed += params.SstoreSetGas + if gas.SubGas(params.SstoreSetGas) != nil { + return gasUsed, ErrOutOfGas + } + // Addition of new lock successful + return gasUsed, nil + } + // Lock exists, increment the current lock counter (but don't store it) + currentLockNumber.Add(currentLockNumber, big1) + } + return gasUsed, errors.New("account has locked too many times, overflows int64") +} diff --git a/core/vm/evm.go b/core/vm/evm.go index 106b6e02e7..1f4bf8ee51 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -28,6 +28,7 @@ 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/log" "github.com/dominant-strategies/go-quai/params" "github.com/holiman/uint256" ) @@ -184,7 +185,7 @@ func (evm *EVM) Interpreter() *EVMInterpreter { // parameters. It also handles any necessary value transfer required and takes // the necessary steps to create accounts and reverses the state in case of an // execution error or failed value transfer. -func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas uint64, value *big.Int) (ret []byte, leftOverGas uint64, err error) { +func (evm *EVM) Call(caller ContractRef, addr common.Address, input []byte, gas uint64, value *big.Int, lock *big.Int) (ret []byte, leftOverGas uint64, err error) { if evm.Config.NoRecursion && evm.depth > 0 { return nil, gas, nil } @@ -196,6 +197,33 @@ 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, ErrInsufficientBalance } + lockupContractAddress := LockupContractAddresses[[2]byte{evm.chainConfig.Location[0], evm.chainConfig.Location[1]}] + if addr.Equal(lockupContractAddress) { + gasUsed, err := RedeemQuai(evm.StateDB, caller.Address(), new(types.GasPool).AddGas(gas), evm.Context.BlockNumber, lockupContractAddress) + if gas > gasUsed { + gas = gas - gasUsed + } else { + gas = 0 + } + if err != nil { + log.Global.Error("RedeemQuai failed", "err", err) + } + return []byte{}, gas, err + } else if lock != nil && lock.Sign() != 0 { + if err := evm.Context.Transfer(evm.StateDB, caller.Address(), lockupContractAddress, value); err != nil { + return nil, gas, err + } + gasUsed, err := AddNewLock(evm.StateDB, addr, new(types.GasPool).AddGas(gas), lock, evm.Context.BlockNumber, lockupContractAddress) + if gas > gasUsed { + gas = gas - gasUsed + } else { + gas = 0 + } + if err != nil { + log.Global.Error("AddNewLock failed", "err", err) + } + return []byte{}, gas, err + } snapshot := evm.StateDB.Snapshot() p, isPrecompile, addr := evm.precompile(addr) internalAddr, err := addr.InternalAndQuaiAddress() @@ -594,11 +622,17 @@ func (evm *EVM) Create2(caller ContractRef, code []byte, gas uint64, endowment * func (evm *EVM) CreateETX(toAddr common.Address, fromAddr common.Address, gas uint64, value *big.Int, data []byte) (ret []byte, leftOverGas uint64, err error) { // Verify address is not in context - if common.IsInChainScope(toAddr.Bytes(), evm.chainConfig.Location) { + if toAddr.IsInQuaiLedgerScope() && common.IsInChainScope(toAddr.Bytes(), evm.chainConfig.Location) { return []byte{}, 0, fmt.Errorf("%x is in chain scope, but CreateETX was called", toAddr) } - if !toAddr.IsInQuaiLedgerScope() { - return []byte{}, 0, fmt.Errorf("%x is not in quai scope, but CreateETX was called", toAddr) + conversion := false + if toAddr.IsInQiLedgerScope() && common.IsInChainScope(toAddr.Bytes(), evm.chainConfig.Location) { + conversion = true + } + if toAddr.IsInQiLedgerScope() && !common.IsInChainScope(toAddr.Bytes(), evm.chainConfig.Location) { + return []byte{}, 0, fmt.Errorf("%x is in qi scope and is not in the same location, but CreateETX was called", toAddr) + } else if conversion && value.Cmp(params.MinQuaiConversionAmount) < 0 { + return []byte{}, 0, fmt.Errorf("CreateETX conversion error: %d is not sufficient value, required amount: %d", value, params.MinQuaiConversionAmount) } if gas < params.ETXGas { return []byte{}, 0, fmt.Errorf("CreateETX error: %d is not sufficient gas, required amount: %d", gas, params.ETXGas) @@ -632,7 +666,7 @@ func (evm *EVM) CreateETX(toAddr common.Address, fromAddr common.Address, gas ui etx := types.NewTx(&etxInner) // check if the etx is eligible to be sent to the to location - if !evm.Context.CheckIfEtxEligible(evm.Context.EtxEligibleSlices, *etx.To().Location()) { + if !conversion && !evm.Context.CheckIfEtxEligible(evm.Context.EtxEligibleSlices, *etx.To().Location()) { return []byte{}, 0, fmt.Errorf("CreateETX error: ETX is not eligible to be sent to %x", etx.To()) } diff --git a/core/vm/instructions.go b/core/vm/instructions.go index 98ea972d62..4e6ecd558a 100644 --- a/core/vm/instructions.go +++ b/core/vm/instructions.go @@ -684,7 +684,7 @@ func opCall(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byt bigVal = value.ToBig() } - ret, returnGas, err := interpreter.evm.Call(scope.Contract, toAddr, args, gas, bigVal) + ret, returnGas, err := interpreter.evm.Call(scope.Contract, toAddr, args, gas, bigVal, nil) if err != nil { temp.Clear() diff --git a/core/worker.go b/core/worker.go index d71ac1e32e..2694295672 100644 --- a/core/worker.go +++ b/core/worker.go @@ -66,7 +66,7 @@ type environment struct { 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 - gasPool *GasPool // available gas used to pack transactions + gasPool *types.GasPool // available gas used to pack transactions coinbase common.Address etxRLimit int // Remaining number of cross-region ETXs that can be included etxPLimit int // Remaining number of cross-prime ETXs that can be included @@ -736,17 +736,51 @@ func (w *worker) commitUncle(env *environment, uncle *types.WorkObjectHeader) er func (w *worker) commitTransaction(env *environment, parent *types.WorkObject, tx *types.Transaction) ([]*types.Log, error) { if tx != nil { if tx.Type() == types.ExternalTxType && tx.To().IsInQiLedgerScope() { - if err := env.gasPool.SubGas(params.CallValueTransferGas); err != nil { - return nil, err - } - if tx.Value().Int64() > types.MaxDenomination { - return nil, fmt.Errorf("tx %032x emits UTXO with value greater than max denomination", tx.Hash()) - } - if err := env.state.CreateUTXO(tx.OriginatingTxHash(), tx.ETXIndex(), types.NewUtxoEntry(types.NewTxOut(uint8(tx.Value().Int64()), tx.To().Bytes()))); err != nil { - return nil, err + gasUsed := env.wo.GasUsed() + txGas := tx.Gas() + if tx.ETXSender().Location().Equal(*tx.To().Location()) { // Quai->Qi conversion + lock := new(big.Int).Add(env.wo.Number(w.hc.NodeCtx()), big.NewInt(params.ConversionLockPeriod)) + primeTerminus := w.hc.GetPrimeTerminus(env.wo) + if primeTerminus == nil { + return nil, errors.New("prime terminus not found") + } + value := misc.QuaiToQi(primeTerminus, tx.Value()) + 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 := uint8(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 + break + } + txGas -= params.CallValueTransferGas + if err := env.gasPool.SubGas(params.CallValueTransferGas); err != nil { + return nil, err + } + gasUsed += params.CallValueTransferGas + // the ETX hash is guaranteed to be unique + if err := env.state.CreateUTXO(tx.Hash(), outputIndex, types.NewUtxoEntry(types.NewTxOut(uint8(denomination), tx.To().Bytes(), lock))); err != nil { + return nil, err + } + outputIndex++ + } + } + } else { + // This Qi ETX should cost more gas + if err := env.gasPool.SubGas(params.CallValueTransferGas); err != nil { + return nil, err + } + if err := env.state.CreateUTXO(tx.OriginatingTxHash(), tx.ETXIndex(), types.NewUtxoEntry(types.NewTxOut(uint8(tx.Value().Uint64()), tx.To().Bytes(), big.NewInt(0)))); err != nil { + return nil, err + } + gasUsed += params.CallValueTransferGas } - gasUsed := env.wo.Header().GasUsed() - gasUsed += params.CallValueTransferGas env.wo.Header().SetGasUsed(gasUsed) env.txs = append(env.txs, tx) return []*types.Log{}, nil // need to make sure this does not modify receipt hash @@ -782,7 +816,7 @@ func (w *worker) commitTransaction(env *environment, parent *types.WorkObject, t func (w *worker) commitTransactions(env *environment, parent *types.WorkObject, etxs []*types.Transaction, txs *types.TransactionsByPriceAndNonce, etxSet *types.EtxSet, interrupt *int32) bool { gasLimit := env.wo.GasLimit if env.gasPool == nil { - env.gasPool = new(GasPool).AddGas(gasLimit()) + env.gasPool = new(types.GasPool).AddGas(gasLimit()) } var coalescedLogs []*types.Log minEtxGas := gasLimit() / params.MinimumEtxGasDivisor @@ -852,7 +886,15 @@ func (w *worker) commitTransactions(env *environment, parent *types.WorkObject, break } if tx.Type() == types.QiTxType { - if err := w.processQiTx(tx, env); err != nil { + txGas := types.CalculateQiTxGas(tx) + if env.gasPool.Gas() < txGas { + w.logger.WithFields(log.Fields{ + "have": env.gasPool, + "want": txGas, + }).Trace("Not enough gas for further transactions") + break + } + if err := w.processQiTx(tx, env, txGas); err != nil { w.logger.WithFields(log.Fields{ "err": err, "tx": tx.Hash().Hex(), @@ -1352,7 +1394,7 @@ func (w *worker) CurrentInfo(header *types.WorkObject) bool { return header.NumberU64(w.hc.NodeCtx())+c_startingPrintLimit > w.hc.CurrentHeader().NumberU64(w.hc.NodeCtx()) } -func (w *worker) processQiTx(tx *types.Transaction, env *environment) error { +func (w *worker) processQiTx(tx *types.Transaction, env *environment, txGas uint64) error { location := w.hc.NodeLocation() if tx.Type() != types.QiTxType { return fmt.Errorf("tx %032x is not a QiTx", tx.Hash()) @@ -1363,19 +1405,27 @@ func (w *worker) processQiTx(tx *types.Transaction, env *environment) error { if tx.ChainId().Cmp(w.chainConfig.ChainID) != 0 { return fmt.Errorf("tx %032x has wrong chain ID", tx.Hash()) } + gasUsed := env.wo.GasUsed() + gasUsed += txGas // the amount of block gas used in this transaction is only the txGas, regardless of ETXs emitted + if err := env.gasPool.SubGas(txGas); err != nil { + return err + } + if gasUsed > env.wo.GasLimit() { + return fmt.Errorf("tx %032x uses too much gas, have used %d out of %d", tx.Hash(), gasUsed, env.wo.GasLimit()) + } - txGas := types.CalculateQiTxGas(tx) - - gasUsed := env.wo.Header().GasUsed() - gasUsed += txGas addresses := make(map[common.AddressBytes]struct{}) totalQitIn := big.NewInt(0) utxosDelete := make([]types.OutPoint, 0) + inputDenominations := make(map[uint8]uint64) for _, txIn := range tx.TxIn() { utxo := env.state.GetUTXO(txIn.PreviousOutPoint.TxHash, txIn.PreviousOutPoint.Index) if utxo == nil { return fmt.Errorf("tx %032x spends non-existent UTXO %032x:%d", tx.Hash(), txIn.PreviousOutPoint.TxHash, txIn.PreviousOutPoint.Index) } + if utxo.Lock != nil && utxo.Lock.Cmp(env.wo.Number(w.hc.NodeCtx())) > 0 { + return fmt.Errorf("tx %032x spends locked UTXO %032x:%d locked until %s", tx.Hash(), txIn.PreviousOutPoint.TxHash, txIn.PreviousOutPoint.Index, utxo.Lock.String()) + } // Perform some spend processing logic denomination := utxo.Denomination @@ -1386,6 +1436,7 @@ func (w *worker) processQiTx(tx *types.Transaction, env *environment) error { types.MaxDenomination) return errors.New(str) } + inputDenominations[denomination] += 1 // Check for duplicate addresses. This also checks for duplicate inputs. if _, exists := addresses[common.AddressBytes(utxo.Address)]; exists { return errors.New("Duplicate address in QiTx inputs: " + common.AddressBytes(utxo.Address).String()) @@ -1396,11 +1447,17 @@ func (w *worker) processQiTx(tx *types.Transaction, env *environment) error { } var ETXRCount int var ETXPCount int - etxs := make([]*types.Transaction, 0) + etxs := make([]*types.ExternalTx, 0) totalQitOut := big.NewInt(0) + totalConvertQitOut := big.NewInt(0) utxosCreate := make(map[types.OutPoint]*types.UtxoEntry) + conversion := false + var convertAddress common.Address + outputDenominations := make(map[uint8]uint64) for txOutIdx, txOut := range tx.TxOut() { - + if txOutIdx > types.MaxOutputIndex { + return errors.New("transaction has too many outputs") + } if txOut.Denomination > types.MaxDenomination { str := fmt.Sprintf("transaction output value of %v is "+ "higher than max allowed value of %v", @@ -1408,17 +1465,27 @@ func (w *worker) processQiTx(tx *types.Transaction, env *environment) error { types.MaxDenomination) return errors.New(str) } + outputDenominations[txOut.Denomination] += 1 totalQitOut.Add(totalQitOut, types.Denominations[txOut.Denomination]) toAddr := common.BytesToAddress(txOut.Address, location) - if !toAddr.IsInQiLedgerScope() { - w.logger.Error(fmt.Errorf("tx %032x emits UTXO with To address not in the Qi ledger scope", tx.Hash())) - return fmt.Errorf("tx [%v] emits UTXO with To address not in the Qi ledger scope", tx.Hash().Hex()) - } if _, exists := addresses[toAddr.Bytes20()]; exists { return errors.New("Duplicate address in QiTx outputs: " + toAddr.String()) } addresses[toAddr.Bytes20()] = struct{}{} + if toAddr.Location().Equal(location) && toAddr.IsInQuaiLedgerScope() { // Qi->Quai conversion + conversion = true + convertAddress = toAddr + if txOut.Denomination < params.MinQiConversionDenomination { + w.logger.Error(fmt.Errorf("tx %032x emits convert UTXO with value %d less than minimum conversion denomination", tx.Hash(), txOut.Denomination)) + return fmt.Errorf("tx %032x emits convert UTXO with value %d less than minimum conversion denomination", tx.Hash(), txOut.Denomination) + } + totalConvertQitOut.Add(totalConvertQitOut, types.Denominations[txOut.Denomination]) // Add to total conversion output for aggregation + outputDenominations[txOut.Denomination] -= 1 // This output no longer exists because it has been aggregated + delete(addresses, toAddr.Bytes20()) + continue + } + if !toAddr.Location().Equal(location) { // This output creates an ETX // Cross-region? if toAddr.Location().CommonDom(location).Context() == common.REGION_CTX { @@ -1436,15 +1503,18 @@ func (w *worker) processQiTx(tx *types.Transaction, env *environment) error { w.logger.Error(fmt.Errorf("tx %032x emits too many cross-prime ETXs for block. emitted: %d, limit: %d", tx.Hash(), ETXPCount, env.etxPLimit)) return fmt.Errorf("tx [%v] emits too many cross-prime ETXs for block. emitted: %d, limit: %d", tx.Hash().Hex(), ETXPCount, env.etxPLimit) } + if !toAddr.IsInQiLedgerScope() { + w.logger.Error(fmt.Errorf("tx %032x emits UTXO with To address not in the Qi ledger scope", tx.Hash())) + return fmt.Errorf("tx [%v] emits UTXO with To address not in the Qi ledger scope", tx.Hash().Hex()) + } // We should require some kind of extra fee here etxInner := types.ExternalTx{Value: big.NewInt(int64(txOut.Denomination)), To: &toAddr, Sender: common.ZeroAddress(location), OriginatingTxHash: tx.Hash(), ETXIndex: uint16(txOutIdx), Gas: params.TxGas} - etx := types.NewTx(&etxInner) primeTerminus := w.hc.GetPrimeTerminus(env.wo) if !w.hc.CheckIfEtxIsEligible(primeTerminus.EtxEligibleSlices(), *toAddr.Location()) { return fmt.Errorf("etx emitted by tx [%v] going to a slice that is not eligible to receive etx %v", tx.Hash().Hex(), *toAddr.Location()) } - etxs = append(etxs, etx) + etxs = append(etxs, &etxInner) w.logger.Debug("Added UTXO ETX to block") } else { // This output creates a normal UTXO @@ -1460,23 +1530,44 @@ func (w *worker) processQiTx(tx *types.Transaction, env *environment) error { return errors.New(str) } txFeeInQit := new(big.Int).Sub(totalQitIn, totalQitOut) - if err := env.gasPool.SubGas(txGas); err != nil { - return err - } // Check tx against required base fee and gas - baseFeeInQi := misc.QuaiToQi(env.wo, env.wo.Header().BaseFee()) - minimumFee := new(big.Int).Mul(baseFeeInQi, big.NewInt(int64(txGas))) + requiredGas := txGas + (uint64(len(etxs)) * params.TxGas) // Each ETX costs extra gas that is paid in the origin + if requiredGas < txGas { + // overflow + return fmt.Errorf("tx %032x has too many ETXs to calculate required gas", tx.Hash()) + } + minimumFeeInQuai := new(big.Int).Mul(big.NewInt(int64(requiredGas)), env.wo.BaseFee()) + minimumFee := misc.QuaiToQi(env.wo, minimumFeeInQuai) if txFeeInQit.Cmp(minimumFee) < 0 { - return fmt.Errorf("tx %032x has insufficient fee for base fee of %d and gas of %d", tx.Hash(), baseFeeInQi.Uint64(), txGas) + return fmt.Errorf("tx %032x has insufficient fee for base fee * gas, have %d want %d", tx.Hash(), txFeeInQit.Uint64(), minimumFee.Uint64()) } - log.Global.Infof("Minimum fee: %d", minimumFee.Int64()) - // Miner gets remainder of fee after base fee + // Miner gets remainder of fee after base fee, except in the convert case txFeeInQit.Sub(txFeeInQit, minimumFee) + if conversion { + // Since this transaction contains a conversion, the rest of the tx gas is given to conversion + remainingTxFeeInQuai := misc.QiToQuai(env.wo, txFeeInQit) + // Fee is basefee * gas, so gas remaining is fee remaining / basefee + remainingGas := new(big.Int).Div(remainingTxFeeInQuai, env.wo.BaseFee()) + if remainingGas.Uint64() > (env.wo.GasLimit() / params.MinimumEtxGasDivisor) { + // Limit ETX gas to max ETX gas limit (the rest is burned) + remainingGas = new(big.Int).SetUint64(env.wo.GasLimit() / params.MinimumEtxGasDivisor) + } + ETXPCount++ // conversion is technically a cross-prime ETX + if ETXPCount > env.etxPLimit { + w.logger.Error(fmt.Errorf("tx %032x emits too many cross-prime ETXs for block. emitted: %d, limit: %d", tx.Hash(), ETXPCount, env.etxPLimit)) + return fmt.Errorf("tx [%v] emits too many cross-prime ETXs for block. emitted: %d, limit: %d", tx.Hash().Hex(), ETXPCount, env.etxPLimit) + } + etxInner := types.ExternalTx{Value: totalConvertQitOut, To: &convertAddress, Sender: common.ZeroAddress(location), OriginatingTxHash: tx.Hash(), Gas: remainingGas.Uint64()} // Value is in Qits not Denomination + etxs = append(etxs, &etxInner) + txFeeInQit.Sub(txFeeInQit, txFeeInQit) // Fee goes entirely to gas to pay for conversion + } env.wo.Header().SetGasUsed(gasUsed) env.etxRLimit -= ETXRCount env.etxPLimit -= ETXPCount - env.etxs = append(env.etxs, etxs...) + for _, etx := range etxs { + env.etxs = append(env.etxs, types.NewTx(etx)) + } env.txs = append(env.txs, tx) env.utxoFees.Add(env.utxoFees, txFeeInQit) for _, utxo := range utxosDelete { diff --git a/internal/quaiapi/api.go b/internal/quaiapi/api.go index 7dd90ab951..925f5492e3 100644 --- a/internal/quaiapi/api.go +++ b/internal/quaiapi/api.go @@ -634,7 +634,7 @@ func DoCall(ctx context.Context, b Backend, args TransactionArgs, blockNrOrHash }() // Execute the message. - gp := new(core.GasPool).AddGas(math.MaxUint64) + gp := new(types.GasPool).AddGas(math.MaxUint64) result, err := core.ApplyMessage(evm, msg, gp) if err := vmError(); err != nil { return nil, err @@ -1256,7 +1256,7 @@ func AccessList(ctx context.Context, b Backend, blockNrOrHash rpc.BlockNumberOrH if err != nil { return nil, 0, nil, err } - res, err := core.ApplyMessage(vmenv, msg, new(core.GasPool).AddGas(msg.Gas())) + res, err := core.ApplyMessage(vmenv, msg, new(types.GasPool).AddGas(msg.Gas())) if err != nil { return nil, 0, nil, fmt.Errorf("failed to apply transaction: %v err: %v", msg, err) } @@ -1531,11 +1531,6 @@ func (s *PublicTransactionPoolAPI) SendRawTransaction(ctx context.Context, input if err != nil { return common.Hash{}, err } - if tx.Type() != types.QiTxType { - if tx.To() != nil && tx.To().IsInQiLedgerScope() { // change after adding Quai->Qi conversion tx type - return common.Hash{}, common.MakeErrQiAddress(tx.To().Hex()) - } - } return SubmitTransaction(ctx, s.b, tx) } diff --git a/p2p/pb/quai_messages.pb.go b/p2p/pb/quai_messages.pb.go index 772ab6162c..df7cf8790d 100644 --- a/p2p/pb/quai_messages.pb.go +++ b/p2p/pb/quai_messages.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.30.0 -// protoc v4.25.1 +// protoc-gen-go v1.32.0 +// protoc v4.25.2 // source: p2p/pb/quai_messages.proto package pb diff --git a/params/protocol_params.go b/params/protocol_params.go index cd85264ced..dffdc4ade2 100644 --- a/params/protocol_params.go +++ b/params/protocol_params.go @@ -18,6 +18,8 @@ package params import ( "math/big" + + "github.com/dominant-strategies/go-quai/common" ) const ( @@ -169,6 +171,10 @@ const ( // should be chosen to give node operators some time to adjust their // infrastructure, if needed, to account for the upcoming network change. TREE_EXPANSION_WAIT_COUNT = 1024 + + ConversionLockPeriod int64 = 10 // The number of zone blocks that a conversion output is locked for + MinQiConversionDenomination = 1 + ConversionConfirmationContext = common.PRIME_CTX // A conversion requires a single coincident Dom confirmation ) var ( @@ -194,4 +200,5 @@ var ( RegionEntropyTarget = big.NewInt(21) // This is TimeFactor*common.NumZonesInRegion DifficultyAdjustmentPeriod = big.NewInt(360) // This is the number of blocks over which the average has to be taken DifficultyAdjustmentFactor int64 = 40 // This is the factor that divides the log of the change in the difficulty + MinQuaiConversionAmount = new(big.Int).Mul(big.NewInt(1), big.NewInt(GWei)) // 0.000000001 Quai ) diff --git a/trie/proto_trienode.pb.go b/trie/proto_trienode.pb.go index aa42437cdd..c306fde3df 100644 --- a/trie/proto_trienode.pb.go +++ b/trie/proto_trienode.pb.go @@ -1,7 +1,7 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.30.0 -// protoc v4.25.1 +// protoc-gen-go v1.32.0 +// protoc v4.25.2 // source: trie/proto_trienode.proto package trie From bce158f05b99f2aa8726ab6795ea17f2e2723061 Mon Sep 17 00:00:00 2001 From: Jonathan Downing Date: Thu, 28 Mar 2024 15:50:11 -0500 Subject: [PATCH 3/3] Added opConvert --- core/vm/instructions.go | 75 +++++++++++++++++++++++++++++++++++++++++ core/vm/jump_table.go | 7 ++++ core/vm/opcodes.go | 3 ++ 3 files changed, 85 insertions(+) diff --git a/core/vm/instructions.go b/core/vm/instructions.go index 4e6ecd558a..ed80c7a35d 100644 --- a/core/vm/instructions.go +++ b/core/vm/instructions.go @@ -916,6 +916,81 @@ func opETX(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte return nil, nil } +// opConvert creates an external transaction that converts Quai to Qi +// the ETX is added to the current context's cache and must go through Prime +// opConvert is intended to be called in a contract +func opConvert(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { + // Pop gas. The actual gas is in interpreter.evm.callGasTemp. + stack := scope.Stack + // We use it as a temporary value + temp := stack.pop() // following opCall protocol + // Pop other call parameters. + addr, uint256Value, etxGasLimit := stack.pop(), stack.pop(), stack.pop() + bigValue := uint256Value.ToBig() + toAddr := common.Bytes20ToAddress(addr.Bytes20(), interpreter.evm.chainConfig.Location) + // Verify address is in shard + if !common.IsInChainScope(toAddr.Bytes(), interpreter.evm.chainConfig.Location) { + temp.Clear() + stack.push(&temp) + fmt.Printf("%x is not in chain scope, but opConvert was called\n", toAddr) + return nil, nil // following opCall protocol + } else if !toAddr.IsInQiLedgerScope() { + temp.Clear() + stack.push(&temp) + fmt.Printf("%x is not in Qi ledger scope, but opConvert was called\n", toAddr) + return nil, nil // following opCall protocol + } else if bigValue.Cmp(params.MinQuaiConversionAmount) < 0 { + temp.Clear() + stack.push(&temp) + fmt.Printf("%x cannot convert less than %d\n", scope.Contract.self.Address(), params.MinQuaiConversionAmount.Uint64()) + return nil, nil // following opCall protocol + } + sender := scope.Contract.self.Address() + internalSender, err := sender.InternalAndQuaiAddress() + if err != nil { + fmt.Printf("%x opConvert error: %s\n", scope.Contract.self.Address(), err.Error()) + return nil, nil + } + + fee := uint256.NewInt(0) + gasPrice, _ := uint256.FromBig(interpreter.evm.GasPrice) + fee.Mul(gasPrice, &etxGasLimit) // optional: add gasPrice (base fee) and gasTipCap + total := uint256.NewInt(0) + total.Add(&uint256Value, fee) + // Fail if we're trying to transfer more than the available balance + if total.Sign() == 0 || !interpreter.evm.Context.CanTransfer(interpreter.evm.StateDB, scope.Contract.self.Address(), total.ToBig()) { + temp.Clear() + stack.push(&temp) + fmt.Printf("%x cannot transfer %d\n", scope.Contract.self.Address(), total.Uint64()) + return nil, nil + } + + interpreter.evm.StateDB.SubBalance(internalSender, total.ToBig()) + + interpreter.evm.ETXCacheLock.RLock() + index := len(interpreter.evm.ETXCache) + interpreter.evm.ETXCacheLock.RUnlock() + if index > math.MaxUint16 { + temp.Clear() + stack.push(&temp) + fmt.Println("opConvert overflow error: too many ETXs in cache") + return nil, nil + } + + // create external transaction + etxInner := types.ExternalTx{Value: bigValue, To: &toAddr, Sender: sender, OriginatingTxHash: interpreter.evm.Hash, ETXIndex: uint16(index), Gas: etxGasLimit.Uint64()} + etx := types.NewTx(&etxInner) + + interpreter.evm.ETXCacheLock.Lock() + interpreter.evm.ETXCache = append(interpreter.evm.ETXCache, etx) + interpreter.evm.ETXCacheLock.Unlock() + + temp.SetOne() // following opCall protocol + stack.push(&temp) + + return nil, nil +} + // opIsAddressInternal is used to determine if an address is internal or external based on the current chain context func opIsAddressInternal(pc *uint64, interpreter *EVMInterpreter, scope *ScopeContext) ([]byte, error) { addr := scope.Stack.peek() diff --git a/core/vm/jump_table.go b/core/vm/jump_table.go index 1da1c386b3..e292dae62f 100644 --- a/core/vm/jump_table.go +++ b/core/vm/jump_table.go @@ -992,5 +992,12 @@ func newInstructionSet() JumpTable { minStack: minStack(1, 1), maxStack: maxStack(1, 1), }, + CONVERT: { + execute: opConvert, + constantGas: params.ETXGas, + minStack: minStack(4, 1), + maxStack: maxStack(4, 1), + writes: true, + }, } } diff --git a/core/vm/opcodes.go b/core/vm/opcodes.go index 8275b760ee..5da1e4123a 100644 --- a/core/vm/opcodes.go +++ b/core/vm/opcodes.go @@ -216,6 +216,7 @@ const ( CREATE2 ETX ISADDRINTERNAL + CONVERT STATICCALL OpCode = 0xfa REVERT OpCode = 0xfd SELFDESTRUCT OpCode = 0xff @@ -383,6 +384,7 @@ var opCodeToString = map[OpCode]string{ CREATE2: "CREATE2", ETX: "ETX", ISADDRINTERNAL: "ISADDRINTERNAL", + CONVERT: "CONVERT", STATICCALL: "STATICCALL", REVERT: "REVERT", SELFDESTRUCT: "SELFDESTRUCT", @@ -546,6 +548,7 @@ var stringToOp = map[string]OpCode{ "SELFDESTRUCT": SELFDESTRUCT, "ETX": ETX, "ISADDRINTERNAL": ISADDRINTERNAL, + "CONVERT": CONVERT, } // StringToOp finds the opcode whose name is stored in `str`.