diff --git a/core/state_processor.go b/core/state_processor.go index 18b511e17..27571168b 100644 --- a/core/state_processor.go +++ b/core/state_processor.go @@ -508,7 +508,25 @@ func (p *StateProcessor) Process(block *types.WorkObject, batch ethdb.Batch) (ty } receipts = append(receipts, receipt) allLogs = append(allLogs, receipt.Logs...) + continue + } + } else if etx.EtxType() == types.WrappingQiType { + if len(etx.Data()) != common.AddressLength { + return nil, nil, nil, nil, 0, 0, 0, nil, nil, fmt.Errorf("wrapping Qi ETX %x has invalid data length", etx.Hash()) + } + if etx.To() == nil { + return nil, nil, nil, nil, 0, 0, 0, nil, nil, fmt.Errorf("wrapping Qi ETX %x has no recipient", etx.Hash()) + } + ownerContractAddr := common.BytesToAddress(etx.Data(), nodeLocation) + if err := vm.WrapQi(statedb, ownerContractAddr, *etx.To(), common.OneInternal(nodeLocation), etx.Value(), nodeLocation); err != nil { + return nil, nil, nil, nil, 0, 0, 0, nil, nil, fmt.Errorf("could not wrap Qi: %v", err) + } + if err := gp.SubGas(params.QiToQuaiConversionGas); err != nil { + return nil, nil, nil, nil, 0, 0, 0, nil, nil, err } + *usedGas += params.QiToQuaiConversionGas + totalEtxGas += params.QiToQuaiConversionGas + continue } // check if the tx is a coinbase tx @@ -1275,6 +1293,9 @@ func ValidateQiTxInputs(tx *types.Transaction, chain ChainContext, db ethdb.Read if tx.ChainId().Cmp(signer.ChainID()) != 0 { return nil, fmt.Errorf("tx %032x has wrong chain ID", tx.Hash()) } + if len(tx.Data()) != 0 && len(tx.Data()) != common.AddressLength { + return nil, fmt.Errorf("tx %v emits UTXO with invalid data length %d", tx.Hash().Hex(), len(tx.Data())) + } totalQitIn := big.NewInt(0) addresses := make(map[common.AddressBytes]struct{}) inputs := make(map[uint]uint64) @@ -1343,6 +1364,7 @@ func ValidateQiTxOutputsAndSignature(tx *types.Transaction, chain ChainContext, totalQitOut := big.NewInt(0) totalConvertQitOut := big.NewInt(0) conversion := false + wrapping := false pubKeys := make([]*btcec.PublicKey, 0, len(tx.TxIn())) addresses := make(map[common.AddressBytes]struct{}) for _, txIn := range tx.TxIn() { @@ -1378,7 +1400,7 @@ func ValidateQiTxOutputsAndSignature(tx *types.Transaction, chain ChainContext, } addresses[toAddr.Bytes20()] = struct{}{} - if toAddr.Location().Equal(location) && toAddr.IsInQuaiLedgerScope() { // Qi->Quai conversion + if toAddr.Location().Equal(location) && toAddr.IsInQuaiLedgerScope() && len(tx.Data()) == 0 { // Qi->Quai conversion conversion = true if currentHeader.NumberU64(common.ZONE_CTX) < params.GoldenAgeForkNumberV2 && txOut.Denomination < params.MinQiConversionDenomination { return nil, fmt.Errorf("tx %v emits UTXO with value %d less than minimum denomination %d", tx.Hash().Hex(), txOut.Denomination, params.MinQiConversionDenomination) @@ -1386,6 +1408,14 @@ func ValidateQiTxOutputsAndSignature(tx *types.Transaction, chain ChainContext, totalConvertQitOut.Add(totalConvertQitOut, types.Denominations[txOut.Denomination]) // Add to total conversion output for aggregation delete(addresses, toAddr.Bytes20()) continue + } else if toAddr.Location().Equal(location) && toAddr.IsInQiLedgerScope() && len(tx.Data()) != 0 { // Quai->Qi wrapping + ownerContract := common.BytesToAddress(tx.Data(), location) + if _, err := ownerContract.InternalAndQuaiAddress(); err != nil { + return nil, err + } + wrapping = true + totalConvertQitOut.Add(totalConvertQitOut, types.Denominations[txOut.Denomination]) // Uses the same path as conversion but takes priority + delete(addresses, toAddr.Bytes20()) } else if toAddr.IsInQuaiLedgerScope() { return nil, fmt.Errorf("tx [%v] emits UTXO with To address not in the Qi ledger scope", tx.Hash().Hex()) } @@ -1444,11 +1474,13 @@ func ValidateQiTxOutputsAndSignature(tx *types.Transaction, chain ChainContext, if txFeeInQuai.Cmp(minimumFeeInQuai) < 0 { return nil, fmt.Errorf("tx %032x has insufficient fee for base fee, have %d want %d", tx.Hash(), txFeeInQuai.Uint64(), minimumFeeInQuai.Uint64()) } - if conversion { - if currentHeader.NumberU64(common.ZONE_CTX) >= params.GoldenAgeForkNumberV2 && totalConvertQitOut.Cmp(types.Denominations[params.MinQiConversionDenomination]) < 0 { - return nil, fmt.Errorf("tx %032x emits convert UTXO with value %d less than minimum conversion denomination", tx.Hash(), totalConvertQitOut.Uint64()) + if conversion && currentHeader.NumberU64(common.ZONE_CTX) >= params.GoldenAgeForkNumberV2 && totalConvertQitOut.Cmp(types.Denominations[params.MinQiConversionDenomination]) < 0 { + return nil, fmt.Errorf("tx %032x emits convert UTXO with value %d less than minimum conversion denomination", tx.Hash(), totalConvertQitOut.Uint64()) + } + if conversion || wrapping { + if conversion && wrapping { + return nil, fmt.Errorf("tx %032x emits both a conversion and a wrapping UTXO", tx.Hash()) } - if currentHeader.NumberU64(common.ZONE_CTX) >= params.GoldenAgeForkNumberV2 { // Since this transaction contains a conversion, check if the required conversion gas is paid // The user must pay this to the miner now, but it is only added to the block gas limit when the ETX is played in the destination @@ -1512,6 +1544,9 @@ func ProcessQiTx(tx *types.Transaction, chain ChainContext, checkSig bool, isFir if currentHeader == nil || batch == nil || gp == nil || usedGas == nil || signer == nil || etxRLimit == nil || etxPLimit == nil { return nil, nil, nil, errors.New("one of the parameters is nil"), nil } + if len(tx.Data()) != 0 && len(tx.Data()) != common.AddressLength { + return nil, nil, nil, fmt.Errorf("tx %v emits UTXO with invalid data length %d", tx.Hash().Hex(), len(tx.Data())), nil + } intrinsicGas := types.CalculateIntrinsicQiTxGas(tx, qiScalingFactor) *usedGas += intrinsicGas if err := gp.SubGas(intrinsicGas); err != nil { @@ -1586,6 +1621,7 @@ func ProcessQiTx(tx *types.Transaction, chain ChainContext, checkSig bool, isFir totalQitOut := big.NewInt(0) totalConvertQitOut := big.NewInt(0) conversion := false + wrapping := 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 @@ -1611,7 +1647,7 @@ func ProcessQiTx(tx *types.Transaction, chain ChainContext, checkSig bool, isFir addresses[toAddr.Bytes20()] = struct{}{} outputs[uint(txOut.Denomination)]++ - if toAddr.Location().Equal(location) && toAddr.IsInQuaiLedgerScope() { // Qi->Quai conversion + if toAddr.Location().Equal(location) && toAddr.IsInQuaiLedgerScope() && len(tx.Data()) == 0 { // Qi->Quai conversion conversion = true convertAddress = toAddr if currentHeader.NumberU64(common.ZONE_CTX) < params.GoldenAgeForkNumberV2 && txOut.Denomination < params.MinQiConversionDenomination { @@ -1621,6 +1657,16 @@ func ProcessQiTx(tx *types.Transaction, chain ChainContext, checkSig bool, isFir outputs[uint(txOut.Denomination)] -= 1 // This output no longer exists because it has been aggregated delete(addresses, toAddr.Bytes20()) continue + } else if toAddr.Location().Equal(location) && toAddr.IsInQuaiLedgerScope() && len(tx.Data()) != 0 { // Wrapped Qi transaction + ownerContract := common.BytesToAddress(tx.Data(), location) + if _, err := ownerContract.InternalAndQuaiAddress(); err != nil { + return nil, nil, nil, err, nil + } + wrapping = true + convertAddress = toAddr + totalConvertQitOut.Add(totalConvertQitOut, types.Denominations[txOut.Denomination]) // Uses the same path as conversion but takes priority + outputs[uint(txOut.Denomination)] -= 1 // This output no longer exists because it has been aggregated + delete(addresses, toAddr.Bytes20()) } else if toAddr.IsInQuaiLedgerScope() { return nil, nil, nil, fmt.Errorf("tx %v emits UTXO with To address not in the Qi ledger scope", tx.Hash().Hex()), nil } @@ -1695,9 +1741,18 @@ func ProcessQiTx(tx *types.Transaction, chain ChainContext, checkSig bool, isFir if txFeeInQuai.Cmp(minimumFeeInQuai) < 0 { return nil, nil, nil, fmt.Errorf("tx %032x has insufficient fee for base fee, have %d want %d", tx.Hash(), txFeeInQuai.Uint64(), minimumFeeInQuai.Uint64()), nil } - if conversion { - if currentHeader.NumberU64(common.ZONE_CTX) >= params.GoldenAgeForkNumberV2 && totalConvertQitOut.Cmp(types.Denominations[params.MinQiConversionDenomination]) < 0 { - return nil, nil, nil, fmt.Errorf("tx %032x emits convert UTXO with value %d less than minimum conversion denomination", tx.Hash(), totalConvertQitOut.Uint64()), nil + if conversion && currentHeader.NumberU64(common.ZONE_CTX) >= params.GoldenAgeForkNumberV2 && totalConvertQitOut.Cmp(types.Denominations[params.MinQiConversionDenomination]) < 0 { + return nil, nil, nil, fmt.Errorf("tx %032x emits convert UTXO with value %d less than minimum conversion denomination", tx.Hash(), totalConvertQitOut.Uint64()), nil + } + if conversion || wrapping { + if conversion && wrapping { + return nil, nil, nil, fmt.Errorf("tx %032x emits both a conversion and a wrapping UTXO", tx.Hash()), nil + } + etxType := types.ConversionType + data := []byte{} + if wrapping { + etxType = types.WrappingQiType + data = tx.Data() } var etxInner types.ExternalTx if currentHeader.NumberU64(common.ZONE_CTX) < params.GoldenAgeForkNumberV2 { @@ -1713,7 +1768,7 @@ func ProcessQiTx(tx *types.Transaction, chain ChainContext, checkSig bool, isFir if ETXPCount > *etxPLimit { return nil, nil, nil, fmt.Errorf("tx [%v] emits too many cross-prime ETXs for block. emitted: %d, limit: %d", tx.Hash().Hex(), ETXPCount, etxPLimit), nil } - etxInner = types.ExternalTx{Value: totalConvertQitOut, To: &convertAddress, Sender: common.ZeroAddress(location), EtxType: types.ConversionType, OriginatingTxHash: tx.Hash(), Gas: remainingGas.Uint64()} // Value is in Qits not Denomination + etxInner = types.ExternalTx{Value: totalConvertQitOut, To: &convertAddress, Sender: common.ZeroAddress(location), EtxType: uint64(etxType), OriginatingTxHash: tx.Hash(), Gas: remainingGas.Uint64(), Data: data} // Value is in Qits not Denomination } else { // Since this transaction contains a conversion, check if the required conversion gas is paid // The user must pay this to the miner now, but it is only added to the block gas limit when the ETX is played in the destination @@ -1726,7 +1781,7 @@ func ProcessQiTx(tx *types.Transaction, chain ChainContext, checkSig bool, isFir if ETXPCount > *etxPLimit { return nil, nil, nil, fmt.Errorf("tx [%v] emits too many cross-prime ETXs for block. emitted: %d, limit: %d", tx.Hash().Hex(), ETXPCount, etxPLimit), nil } - etxInner = types.ExternalTx{Value: totalConvertQitOut, To: &convertAddress, Sender: common.ZeroAddress(location), EtxType: types.ConversionType, OriginatingTxHash: tx.Hash(), Gas: 0} // Value is in Qits not Denomination + etxInner = types.ExternalTx{Value: totalConvertQitOut, To: &convertAddress, Sender: common.ZeroAddress(location), EtxType: uint64(etxType), OriginatingTxHash: tx.Hash(), Gas: 0, Data: data} // Value is in Qits not Denomination } *usedGas += params.ETXGas if err := gp.SubGas(params.ETXGas); err != nil { diff --git a/core/types/qi_tx.go b/core/types/qi_tx.go index 29a9c682b..0a2f4f282 100644 --- a/core/types/qi_tx.go +++ b/core/types/qi_tx.go @@ -14,6 +14,7 @@ type QiTx struct { TxOut TxOuts `json:"txOuts"` Signature *schnorr.Signature + Data []byte // Data is currently only used for wrapping Qi in the EVM // Work fields ParentHash *common.Hash @@ -103,7 +104,7 @@ func (tx *QiTx) parentHash() *common.Hash { return tx.ParentHash func (tx *QiTx) mixHash() *common.Hash { return tx.MixHash } func (tx *QiTx) workNonce() *BlockNonce { return tx.WorkNonce } func (tx *QiTx) accessList() AccessList { panic("Qi TX does not have accessList") } -func (tx *QiTx) data() []byte { panic("Qi TX does not have data") } +func (tx *QiTx) data() []byte { return tx.Data } func (tx *QiTx) gas() uint64 { panic("Qi TX does not have gas") } func (tx *QiTx) minerTip() *big.Int { panic("Qi TX does not have minerTip") } func (tx *QiTx) gasPrice() *big.Int { panic("Qi TX does not have gasPrice") } diff --git a/core/types/transaction.go b/core/types/transaction.go index c2d9f3664..c1371baaf 100644 --- a/core/types/transaction.go +++ b/core/types/transaction.go @@ -55,6 +55,7 @@ const ( CoinbaseType ConversionType CoinbaseLockupType + WrappingQiType ) const ( @@ -222,7 +223,11 @@ func (tx *Transaction) ProtoEncode() (*ProtoTransaction, error) { workNonce := tx.WorkNonce().Uint64() protoTx.WorkNonce = &workNonce } - + if tx.Data() == nil { + protoTx.Data = []byte{} + } else { + protoTx.Data = tx.Data() + } } return protoTx, nil } @@ -373,6 +378,9 @@ func (tx *Transaction) ProtoDecode(protoTx *ProtoTransaction, location common.Lo if protoTx.ChainId == nil { return errors.New("missing required field 'ChainId' in ProtoTransaction") } + if protoTx.Data == nil { + return errors.New("missing required field 'Data' in ProtoTransaction") + } var qiTx QiTx qiTx.ChainID = new(big.Int).SetBytes(protoTx.GetChainId()) @@ -411,6 +419,7 @@ func (tx *Transaction) ProtoDecode(protoTx *ProtoTransaction, location common.Lo nonce := BlockNonce(uint64ToByteArr(*protoTx.WorkNonce)) qiTx.WorkNonce = &nonce } + qiTx.Data = protoTx.GetData() tx.SetInner(&qiTx) default: @@ -454,6 +463,11 @@ func (tx *Transaction) ProtoEncodeTxSigningData() *ProtoTransaction { protoTxSigningData.ChainId = tx.ChainId().Bytes() protoTxSigningData.TxIns, _ = tx.TxIn().ProtoEncode() protoTxSigningData.TxOuts, _ = tx.TxOut().ProtoEncode() + if tx.Data() == nil { + protoTxSigningData.Data = []byte{} + } else { + protoTxSigningData.Data = tx.Data() + } } return protoTxSigningData } diff --git a/core/vm/contracts.go b/core/vm/contracts.go index 593829657..90ec801d3 100644 --- a/core/vm/contracts.go +++ b/core/vm/contracts.go @@ -506,8 +506,23 @@ func RequiredGas(input []byte) uint64 { return 0 } +// RunLockupContract is an API that ties together the Lockup Contract with the EVM. +// The requested function is determined by input length. Contracts should ensure that their input is tightly packed +// with abi.encodePacked. func RunLockupContract(evm *EVM, ownerContract common.Address, gas *uint64, input []byte) ([]byte, error) { switch len(input) { + case 60: + if err := UnwrapQi(evm, ownerContract, gas, input); err != nil { + return nil, err + } else { + return []byte{1}, nil + } + case 20: + if err := ClaimQiDeposit(evm, ownerContract, gas, input); err != nil { + return nil, err + } else { + return []byte{1}, nil + } case 33: if err := ClaimCoinbaseLockup(evm, ownerContract, evm.Context.BlockNumber.Uint64(), gas, input); err != nil { return nil, err @@ -768,3 +783,148 @@ func AddNewLock(statedb StateDB, batch ethdb.Batch, ownerContract common.Address } return oldKey, newKey, oldCoinbaseLockupHashPtr, &newCoinbaseLockupHash, nil } + +// UnwrapQi is called by a smart contract that owns wrapped Qi to unwrap it for real Qi UTXOs +// It deducts the requested Qi balance from the contract's balance and creates an external transaction to the beneficiary +// It is the responsibility of the contract to ensure solvency in its underyling wrapped Qi balance +func UnwrapQi(evm *EVM, ownerContract common.Address, gas *uint64, input []byte) error { + // input is tightly packed 20 bytes for Qi beneficiary, 32 bytes for value and 8 bytes for etxGasLimit + if len(input) != 60 { + return errors.New("input length is not 36 bytes") + } + beneficiaryQi := common.BytesToAddress(input[:20], evm.chainConfig.Location) + value := new(big.Int).SetBytes(input[20:52]) + etxGasLimit := binary.BigEndian.Uint64(input[52:60]) + + if *gas < etxGasLimit { + return ErrOutOfGas + } + *gas -= etxGasLimit + + lockupContractAddress := LockupContractAddresses[[2]byte{evm.chainConfig.Location[0], evm.chainConfig.Location[1]}] + lockupContractAddressInternal, err := lockupContractAddress.InternalAndQuaiAddress() + if err != nil { + return err + } + + _, err = beneficiaryQi.InternalAndQiAddress() + if err != nil { + return err + } + ownerContractInternal, err := ownerContract.InternalAndQuaiAddress() + if err != nil { + return err + } + ownerContractHash := common.BytesToHash(ownerContractInternal[:]) + + balanceHash := evm.StateDB.GetState(lockupContractAddressInternal, ownerContractHash) + if balanceHash == (common.Hash{}) { + return errors.New("no wrapped Qi balance in contract to unwrap") + } + balanceBig := balanceHash.Big() + if balanceBig.Cmp(value) == -1 { + return fmt.Errorf("not enough wrapped Qi to unwrap: have %v want %v", balanceBig, value) + } + balanceBig.Sub(balanceBig, value) + evm.StateDB.SetState(lockupContractAddressInternal, ownerContractHash, common.BigToHash(balanceBig)) + + 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: value, To: &beneficiaryQi, 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.ETXCacheLock.Unlock() + return nil +} + +// WrapQi is called by the state processor to process an inbound Qi wrapping ETX +// It stores the wrapped Qi balance in the lockup contract keyed with the contract address and provided Quai beneficiary address +// To accept the deposit, the smart contract must call the ClaimQiDeposit function on the precompile +func WrapQi(statedb StateDB, ownerContract, beneficiary common.Address, sender common.InternalAddress, value *big.Int, location common.Location) error { + ownerContractInternal, err := ownerContract.InternalAndQuaiAddress() + if err != nil { + return err + } + beneficiaryInternal, err := beneficiary.InternalAndQuaiAddress() + if err != nil { + return err + } + if sender != common.OneInternal(location) { + return errors.New("sender is not the correct internal address") + } + lockupContractAddress := LockupContractAddresses[[2]byte{location[0], location[1]}] + lockupContractAddressInternal, err := lockupContractAddress.InternalAndQuaiAddress() + if err != nil { + return err + } + + if value.Sign() == -1 { + return errors.New("negative value") + } + if value.Sign() == 0 { + return nil + } + wrappedQiKey := common.Hash{} + copy(wrappedQiKey[:16], ownerContractInternal[:16]) + copy(wrappedQiKey[16:], beneficiaryInternal[:16]) + balanceHash := statedb.GetState(lockupContractAddressInternal, wrappedQiKey) + if (balanceHash == common.Hash{}) { + statedb.SetState(lockupContractAddressInternal, wrappedQiKey, common.BigToHash(value)) + } else { + balanceBig := balanceHash.Big() + balanceBig.Add(balanceBig, value) + statedb.SetState(lockupContractAddressInternal, wrappedQiKey, common.BigToHash(balanceBig)) + } + return nil +} + +// ClaimQiDeposit is called by the owner smart contract to claim a wrapped Qi deposit +// It adds the wrapped Qi balance to the smart contract's Wrapped Qi balance +// The contract should then mint the equivalent amount of Wrapped Qi tokens to the Quai beneficiary +func ClaimQiDeposit(evm *EVM, ownerContract common.Address, gas *uint64, input []byte) error { + // input is tightly packed 20 bytes for Quai owner + if len(input) != 20 { + return errors.New("input length is not 20 bytes") + } + quaiOwner := common.BytesToAddress(input, evm.chainConfig.Location) + lockupContractAddress := LockupContractAddresses[[2]byte{evm.chainConfig.Location[0], evm.chainConfig.Location[1]}] + lockupContractAddressInternal, err := lockupContractAddress.InternalAndQuaiAddress() + if err != nil { + return err + } + ownerContractInternal, err := ownerContract.InternalAndQuaiAddress() + if err != nil { + return err + } + ownerQuaiInternal, err := quaiOwner.InternalAndQuaiAddress() + if err != nil { + return err + } + + wrappedQiKey := common.Hash{} + copy(wrappedQiKey[:16], ownerContractInternal[:16]) + copy(wrappedQiKey[16:], ownerQuaiInternal[:16]) + balanceHash := evm.StateDB.GetState(lockupContractAddressInternal, wrappedQiKey) + if balanceHash == (common.Hash{}) { + return errors.New("no wrapped Qi balance to claim") + } + evm.StateDB.SetState(lockupContractAddressInternal, wrappedQiKey, common.Hash{}) + + ownerContractHash := common.BytesToHash(ownerContractInternal[:]) + ownerContractBalanceHash := evm.StateDB.GetState(lockupContractAddressInternal, ownerContractHash) + if ownerContractBalanceHash == (common.Hash{}) { + evm.StateDB.SetState(lockupContractAddressInternal, ownerContractHash, balanceHash) + } else { + ownerContractBalance := ownerContractBalanceHash.Big() + ownerContractBalance.Add(ownerContractBalance, balanceHash.Big()) + evm.StateDB.SetState(lockupContractAddressInternal, ownerContractHash, common.BigToHash(ownerContractBalance)) + } + + return nil +} diff --git a/core/worker.go b/core/worker.go index abd734e82..273006831 100644 --- a/core/worker.go +++ b/core/worker.go @@ -1452,6 +1452,22 @@ func (w *worker) commitTransaction(env *environment, parent *types.WorkObject, t env.gasUsedAfterTransaction = append(env.gasUsedAfterTransaction, gasUsed) env.txs = append(env.txs, tx) return []*types.Log{}, false, nil // The conversion is locked and will be redeemed later + } else if tx.Type() == types.ExternalTxType && tx.EtxType() == types.WrappingQiType && tx.To().IsInQuaiLedgerScope() { // Qi wrapping ETX + if len(tx.Data()) != common.AddressLength { + return nil, false, fmt.Errorf("wrapping Qi ETX %x has invalid data length", tx.Hash()) + } + if tx.To() == nil { + return nil, false, fmt.Errorf("wrapping Qi ETX %x has no recipient", tx.Hash()) + } + ownerContractAddr := common.BytesToAddress(tx.Data(), w.chainConfig.Location) + if err := vm.WrapQi(env.state, ownerContractAddr, *tx.To(), common.OneInternal(w.chainConfig.Location), tx.Value(), w.chainConfig.Location); err != nil { + return nil, false, fmt.Errorf("could not wrap Qi: %w", err) + } + gasUsed := env.wo.GasUsed() + params.QiToQuaiConversionGas + env.wo.Header().SetGasUsed(gasUsed) + env.gasUsedAfterTransaction = append(env.gasUsedAfterTransaction, gasUsed) + env.txs = append(env.txs, tx) + return []*types.Log{}, false, nil } snap := env.state.Snapshot() // retrieve the gas used int and pass in the reference to the ApplyTransaction @@ -2268,6 +2284,9 @@ func (w *worker) processQiTx(tx *types.Transaction, env *environment, primeTermi if tx.ChainId().Cmp(w.chainConfig.ChainID) != 0 { return fmt.Errorf("tx %032x has wrong chain ID", tx.Hash()) } + if len(tx.Data()) != 0 && len(tx.Data()) != common.AddressLength { + return fmt.Errorf("tx %v emits UTXO with invalid data length %d", tx.Hash().Hex(), len(tx.Data())) + } gasUsed := env.wo.GasUsed() intrinsicGas := types.CalculateIntrinsicQiTxGas(tx, env.qiGasScalingFactor) gasUsed += intrinsicGas // the amount of block gas used in this transaction is only the txGas, regardless of ETXs emitted @@ -2314,6 +2333,7 @@ func (w *worker) processQiTx(tx *types.Transaction, env *environment, primeTermi totalConvertQitOut := big.NewInt(0) utxosCreateHashes := make([]common.Hash, 0, len(tx.TxOut())) conversion := false + wrapping := false var convertAddress common.Address outputs := make(map[uint]uint64) for txOutIdx, txOut := range tx.TxOut() { @@ -2338,7 +2358,7 @@ func (w *worker) processQiTx(tx *types.Transaction, env *environment, primeTermi } addresses[toAddr.Bytes20()] = struct{}{} - if toAddr.Location().Equal(location) && toAddr.IsInQuaiLedgerScope() { // Qi->Quai conversion + if toAddr.Location().Equal(location) && toAddr.IsInQuaiLedgerScope() && len(tx.Data()) == 0 { // Qi->Quai conversion if conversion && !toAddr.Equal(convertAddress) { // All convert outputs must have the same To address for aggregation return fmt.Errorf("tx %032x emits multiple convert UTXOs with different To addresses", tx.Hash()) } @@ -2351,6 +2371,16 @@ func (w *worker) processQiTx(tx *types.Transaction, env *environment, primeTermi outputs[uint(txOut.Denomination)] -= 1 // This output no longer exists because it has been aggregated delete(addresses, toAddr.Bytes20()) continue + } else if toAddr.Location().Equal(location) && toAddr.IsInQuaiLedgerScope() && len(tx.Data()) != 0 { // Wrapped Qi transaction + ownerContract := common.BytesToAddress(tx.Data(), location) + if _, err := ownerContract.InternalAndQuaiAddress(); err != nil { + return err + } + wrapping = true + convertAddress = toAddr + totalConvertQitOut.Add(totalConvertQitOut, types.Denominations[txOut.Denomination]) // Uses the same path as conversion but takes priority + outputs[uint(txOut.Denomination)] -= 1 // This output no longer exists because it has been aggregated + delete(addresses, toAddr.Bytes20()) } else if toAddr.IsInQuaiLedgerScope() { return fmt.Errorf("tx %032x emits UTXO with To address not in the Qi ledger scope", tx.Hash()) } @@ -2422,9 +2452,18 @@ func (w *worker) processQiTx(tx *types.Transaction, env *environment, primeTermi if txFeeInQuai.Cmp(minimumFeeInQuai) < 0 { return fmt.Errorf("tx %032x has insufficient fee for base fee * gas, have %d want %d", tx.Hash(), txFeeInQit.Uint64(), minimumFeeInQuai.Uint64()) } - if conversion { - if env.wo.NumberU64(common.ZONE_CTX) >= params.GoldenAgeForkNumberV2 && totalConvertQitOut.Cmp(types.Denominations[params.MinQiConversionDenomination]) < 0 { - return fmt.Errorf("tx %032x emits convert UTXO with value %d less than minimum conversion denomination", tx.Hash(), totalConvertQitOut.Uint64()) + if conversion && env.wo.NumberU64(common.ZONE_CTX) >= params.GoldenAgeForkNumberV2 && totalConvertQitOut.Cmp(types.Denominations[params.MinQiConversionDenomination]) < 0 { + return fmt.Errorf("tx %032x emits convert UTXO with value %d less than minimum conversion denomination", tx.Hash(), totalConvertQitOut.Uint64()) + } + if conversion || wrapping { + if conversion && wrapping { + return fmt.Errorf("tx %032x emits both a conversion and a wrapping UTXO", tx.Hash()) + } + etxType := types.ConversionType + data := []byte{} + if wrapping { + etxType = types.WrappingQiType + data = tx.Data() } var etxInner types.ExternalTx if env.wo.NumberU64(common.ZONE_CTX) < params.GoldenAgeForkNumberV2 { @@ -2444,7 +2483,7 @@ func (w *worker) processQiTx(tx *types.Transaction, env *environment, primeTermi if 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), EtxType: types.ConversionType, OriginatingTxHash: tx.Hash(), Gas: remainingGas.Uint64()} // Value is in Qits not Denomination + etxInner = types.ExternalTx{Value: totalConvertQitOut, To: &convertAddress, Sender: common.ZeroAddress(location), EtxType: uint64(etxType), OriginatingTxHash: tx.Hash(), Gas: remainingGas.Uint64(), Data: data} // Value is in Qits not Denomination } else { // Since this transaction contains a conversion, check if the required conversion gas is paid // The user must pay this to the miner now, but it is only added to the block gas limit when the ETX is played in the destination @@ -2458,7 +2497,7 @@ func (w *worker) processQiTx(tx *types.Transaction, env *environment, primeTermi return fmt.Errorf("tx [%v] emits too many cross-prime ETXs for block. emitted: %d, limit: %d", tx.Hash().Hex(), ETXPCount, env.etxPLimit) } // Value is in Qits not Denomination - etxInner = types.ExternalTx{Value: totalConvertQitOut, To: &convertAddress, Sender: common.ZeroAddress(location), EtxType: types.ConversionType, OriginatingTxHash: tx.Hash(), Gas: 0} // Conversion gas is paid from the converted Quai balance (for new account creation, when redeemed) + etxInner = types.ExternalTx{Value: totalConvertQitOut, To: &convertAddress, Sender: common.ZeroAddress(location), EtxType: uint64(etxType), OriginatingTxHash: tx.Hash(), Gas: 0, Data: data} // Conversion gas is paid from the converted Quai balance (for new account creation, when redeemed) } gasUsed += params.ETXGas if err := env.gasPool.SubGas(params.ETXGas); err != nil {