From 9da00cd8c0a4a5124d47420777b4fa76b0bb90a8 Mon Sep 17 00:00:00 2001 From: Alex Forshtat Date: Mon, 29 Apr 2024 17:29:07 +0200 Subject: [PATCH] WIP: manually bring over RIP-7560 related code; compiles but does not run --- core/state_processor_rip7560.go | 653 +++++++++++++++++++++++++ core/state_transition.go | 2 + core/txpool/blobpool/blobpool.go | 15 + core/txpool/legacypool/legacypool.go | 17 + core/txpool/rip7560pool/rip7560pool.go | 273 +++++++++++ core/txpool/subpool.go | 6 + core/txpool/txpool_rip7560.go | 46 ++ core/types/transaction.go | 7 + core/types/tx_rip7560.go | 221 +++++++++ eth/api_backend_rip7560.go | 15 + eth/backend.go | 9 +- internal/ethapi/backend.go | 4 + internal/ethapi/rip7560api.go | 62 +++ miner/worker.go | 24 + params/config.go | 5 + 15 files changed, 1358 insertions(+), 1 deletion(-) create mode 100644 core/state_processor_rip7560.go create mode 100644 core/txpool/rip7560pool/rip7560pool.go create mode 100644 core/txpool/txpool_rip7560.go create mode 100644 core/types/tx_rip7560.go create mode 100644 eth/api_backend_rip7560.go create mode 100644 internal/ethapi/rip7560api.go diff --git a/core/state_processor_rip7560.go b/core/state_processor_rip7560.go new file mode 100644 index 0000000000..c0c3d9f7ae --- /dev/null +++ b/core/state_processor_rip7560.go @@ -0,0 +1,653 @@ +package core + +import ( + "encoding/binary" + "errors" + "fmt" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/state" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/params" + "github.com/holiman/uint256" + "math/big" + "strings" +) + +type ValidationPhaseResult struct { + TxIndex int + Tx *types.Transaction + TxHash common.Hash + PaymasterContext []byte + DeploymentUsedGas uint64 + ValidationUsedGas uint64 + PmValidationUsedGas uint64 + SenderValidAfter uint64 + SenderValidUntil uint64 + PmValidAfter uint64 + PmValidUntil uint64 + IsEOA bool +} + +// HandleRip7560Transactions apply state changes of all sequential RIP-7560 transactions and return +// the number of handled transactions +// the transactions array must start with the RIP-7560 transaction +func HandleRip7560Transactions(transactions []*types.Transaction, index int, statedb *state.StateDB, coinbase *common.Address, header *types.Header, gp *GasPool, chainConfig *params.ChainConfig, bc ChainContext, cfg vm.Config) ([]*types.Transaction, types.Receipts, []*types.Log, error) { + validatedTransactions := make([]*types.Transaction, 0) + receipts := make([]*types.Receipt, 0) + allLogs := make([]*types.Log, 0) + + i := index + for { + if i >= len(transactions) { + break + } + if transactions[i].Type() != types.Rip7560Type { + break + } + iTransactions, iReceipts, iLogs, err := handleRip7560Transactions(transactions, index, statedb, coinbase, header, gp, chainConfig, bc, cfg) + if err != nil { + return nil, nil, nil, err + } + validatedTransactions = append(validatedTransactions, iTransactions...) + receipts = append(receipts, iReceipts...) + allLogs = append(allLogs, iLogs...) + } + return validatedTransactions, receipts, allLogs, nil +} + +func handleRip7560Transactions(transactions []*types.Transaction, index int, statedb *state.StateDB, coinbase *common.Address, header *types.Header, gp *GasPool, chainConfig *params.ChainConfig, bc ChainContext, cfg vm.Config) ([]*types.Transaction, types.Receipts, []*types.Log, error) { + validationPhaseResults := make([]*ValidationPhaseResult, 0) + validatedTransactions := make([]*types.Transaction, 0) + receipts := make([]*types.Receipt, 0) + allLogs := make([]*types.Log, 0) + signer := types.MakeSigner(chainConfig, header.Number, header.Time) + for i, tx := range transactions[index:] { + if tx.Type() != types.Rip7560Type { + break + } + + aatx := tx.Rip7560TransactionData() + isEoa, err := isTransactionEOA(tx, statedb, signer) + if err != nil { + return nil, nil, nil, err + } + statedb.SetTxContext(tx.Hash(), index+i) + err = BuyGasRip7560Transaction(aatx, statedb) + var vpr *ValidationPhaseResult + if isEoa { + blockContext := NewEVMBlockContext(header, bc, coinbase) + tmpMsg, err := TransactionToMessage(tx, signer, header.BaseFee) + txContext := NewEVMTxContext(tmpMsg) + evm := vm.NewEVM(blockContext, txContext, statedb, chainConfig, cfg) + //signer := types.MakeSigner(chainConfig, header.Number, header.Time) + signingHash := signer.Hash(tx) + vpr, err = validateRip7560TransactionFromEOA(tx, signingHash, statedb, evm, coinbase, header, gp, chainConfig) + if err != nil { + return nil, nil, nil, err + } + } else { + if err != nil { + return nil, nil, nil, err + } + vpr, err = ApplyRip7560ValidationPhases(chainConfig, bc, coinbase, gp, statedb, header, tx, cfg) + if err != nil { + return nil, nil, nil, err + } + } + validationPhaseResults = append(validationPhaseResults, vpr) + validatedTransactions = append(validatedTransactions, tx) + } + for i, vpr := range validationPhaseResults { + + // TODO: this will miss all validation phase events - pass in 'vpr' + statedb.SetTxContext(vpr.Tx.Hash(), i) + + receipt, err := ApplyRip7560ExecutionPhase(chainConfig, vpr, bc, coinbase, gp, statedb, header, cfg) + + if err != nil { + return nil, nil, nil, err + } + receipts = append(receipts, receipt) + allLogs = append(allLogs, receipt.Logs...) + } + return validatedTransactions, receipts, allLogs, nil +} + +// BuyGasRip7560Transaction +// todo: move to a suitable interface, whatever that is +// todo 2: maybe handle the "shared gas pool" situation instead of just overriding it completely? +func BuyGasRip7560Transaction(st *types.Rip7560AccountAbstractionTx, state vm.StateDB) error { + gasLimit := st.Gas + st.ValidationGas + st.PaymasterGas + st.PostOpGas + mgval := new(uint256.Int).SetUint64(gasLimit) + gasFeeCap, _ := uint256.FromBig(st.GasFeeCap) + mgval = mgval.Mul(mgval, gasFeeCap) + balanceCheck := new(uint256.Int).Set(mgval) + + chargeFrom := *st.Sender + + if len(st.PaymasterData) >= 20 { + chargeFrom = [20]byte(st.PaymasterData[:20]) + } + + if have, want := state.GetBalance(chargeFrom), balanceCheck; have.Cmp(want) < 0 { + return fmt.Errorf("%w: address %v have %v want %v", ErrInsufficientFunds, chargeFrom.Hex(), have, want) + } + + state.SubBalance(chargeFrom, mgval, 0) + return nil +} + +// TODO: not needed with subtype - only use to validate transaction, maybe +func isTransactionEOA(tx *types.Transaction, statedb *state.StateDB, signer types.Signer) (bool, error) { + aatx := tx.Rip7560TransactionData() + senderHasCode := statedb.GetCodeSize(*aatx.Sender) != 0 || len(aatx.DeployerData) != 0 + if senderHasCode { + return false, nil + } + address, err := signer.Sender(tx) + if err != nil { + return false, err + } + if address.Cmp(*tx.Rip7560TransactionData().Sender) != 0 { + return false, errors.New("recovered signature does not match the claimed EOA sender") + } + return true, nil +} + +func validateRip7560TransactionFromEOA(tx *types.Transaction, signingHash common.Hash, statedb *state.StateDB, evm *vm.EVM, coinbase *common.Address, header *types.Header, gp *GasPool, chainConfig *params.ChainConfig) (*ValidationPhaseResult, error) { + // TODO: paymaste is actually optional for eoa-type-4 -> check paymaster data len() + paymasterContext, pmValidationUsedGas, pmValidAfter, pmValidUntil, err := applyPaymasterValidationFrame(tx, chainConfig, signingHash, evm, gp, statedb, header) + if err != nil { + return nil, err + } + err = validateValidityTimeRange(header.Time, pmValidAfter, pmValidUntil) + if err != nil { + return nil, err + } + vpr := &ValidationPhaseResult{ + Tx: tx, + TxHash: tx.Hash(), + PaymasterContext: paymasterContext, + DeploymentUsedGas: 0, + ValidationUsedGas: 0, + PmValidationUsedGas: pmValidationUsedGas, + SenderValidAfter: 0, + SenderValidUntil: 0, + PmValidAfter: pmValidAfter, + PmValidUntil: pmValidUntil, + IsEOA: true, + } + return vpr, nil +} + +func ApplyRip7560FrameMessage(evm *vm.EVM, msg *Message, gp *GasPool) (*ExecutionResult, error) { + return NewRip7560StateTransition(evm, msg, gp).TransitionDb() +} + +// NewRip7560StateTransition initialises and returns a new state transition object. +func NewRip7560StateTransition(evm *vm.EVM, msg *Message, gp *GasPool) *StateTransition { + return &StateTransition{ + gp: gp, + evm: evm, + msg: msg, + state: evm.StateDB, + rip7560Frame: true, + } +} + +// GetRip7560AccountNonce reads the two-dimensional RIP-7560 nonce from the given blockchain state +func GetRip7560AccountNonce(config *params.ChainConfig, bc ChainContext, author *common.Address, gp *GasPool, statedb *state.StateDB, header *types.Header, tx *types.Transaction, cfg vm.Config, sender common.Address, nonceKey *big.Int) uint64 { + + // todo: this is a copy paste of 5 lines that need 8 parameters to run, wtf? + blockContext := NewEVMBlockContext(header, bc, author) + message, err := TransactionToMessage(tx, types.MakeSigner(config, header.Number, header.Time), header.BaseFee) + txContext := NewEVMTxContext(message) + vmenv := vm.NewEVM(blockContext, txContext, statedb, config, cfg) + vmenv.Reset(txContext, statedb) // TODO what does this 'reset' do? + + from := common.HexToAddress("0x0000000000000000000000000000000000000000") + // todo: read NM address from global config + nonceManager := common.HexToAddress("0xdebc121d1b09bc03ff57fa1f96514d04a1f0f59d") + fromBigNonceKey256, _ := uint256.FromBig(nonceKey) + key := make([]byte, 24) + fromBigNonceKey256.WriteToSlice(key) + nonceManagerData := make([]byte, 0) + nonceManagerData = append(nonceManagerData[:], sender.Bytes()...) + nonceManagerData = append(nonceManagerData[:], key...) + + nonceManagerMsg := &Message{ + From: from, + To: &nonceManager, + Value: big.NewInt(0), + GasLimit: 100000, + GasPrice: big.NewInt(875000000), + GasFeeCap: big.NewInt(875000000), + GasTipCap: big.NewInt(875000000), + Data: nonceManagerData, + AccessList: make(types.AccessList, 0), + SkipAccountChecks: true, + IsRip7560Frame: true, + } + resultNonceManager, err := ApplyRip7560FrameMessage(vmenv, nonceManagerMsg, gp) + if err != nil { + // todo: handle + return 777 + } + if resultNonceManager.Err != nil { + return 888 + } + if resultNonceManager.ReturnData == nil { + return 999 + } + return big.NewInt(0).SetBytes(resultNonceManager.ReturnData).Uint64() +} + +func ApplyRip7560ValidationPhases(chainConfig *params.ChainConfig, bc ChainContext, author *common.Address, gp *GasPool, statedb *state.StateDB, header *types.Header, tx *types.Transaction, cfg vm.Config) (*ValidationPhaseResult, error) { + /*** Nonce Manger Frame ***/ + nonceManagerMsg := prepareNonceManagerMessage(tx, chainConfig) + + blockContext := NewEVMBlockContext(header, bc, author) + txContext := NewEVMTxContext(nonceManagerMsg) + txContext.Origin = *tx.Rip7560TransactionData().Sender + evm := vm.NewEVM(blockContext, txContext, statedb, chainConfig, cfg) + + resultNonceManager, err := ApplyRip7560FrameMessage(evm, nonceManagerMsg, gp) + if err != nil { + return nil, err + } + statedb.IntermediateRoot(true) + if resultNonceManager.Err != nil { + return nil, resultNonceManager.Err + } + + /*** Deployer Frame ***/ + deployerMsg := prepareDeployerMessage(tx, chainConfig) + var deploymentUsedGas uint64 + if deployerMsg != nil { + resultDeployer, err := ApplyRip7560FrameMessage(evm, deployerMsg, gp) + if err != nil { + return nil, err + } + statedb.IntermediateRoot(true) + if resultDeployer.Failed() { + // TODO: bubble up the inner error message to the user, if possible + return nil, errors.New("account deployment failed - invalid transaction") + } + deploymentUsedGas = resultDeployer.UsedGas + } + + /*** Account Validation Frame ***/ + signer := types.MakeSigner(chainConfig, header.Number, header.Time) + signingHash := signer.Hash(tx) + accountValidationMsg, err := prepareAccountValidationMessage(tx, chainConfig, signingHash, deploymentUsedGas) + resultAccountValidation, err := ApplyRip7560FrameMessage(evm, accountValidationMsg, gp) + if err != nil { + return nil, err + } + statedb.IntermediateRoot(true) + if resultAccountValidation.Err != nil { + return nil, resultAccountValidation.Err + } + validAfter, validUntil, err := validateAccountReturnData(resultAccountValidation.ReturnData) + if err != nil { + return nil, err + } + err = validateValidityTimeRange(header.Time, validAfter, validUntil) + if err != nil { + return nil, err + } + + paymasterContext, pmValidationUsedGas, pmValidAfter, pmValidUntil, err := applyPaymasterValidationFrame(tx, chainConfig, signingHash, evm, gp, statedb, header) + vpr := &ValidationPhaseResult{ + Tx: tx, + TxHash: tx.Hash(), + PaymasterContext: paymasterContext, + DeploymentUsedGas: deploymentUsedGas, + ValidationUsedGas: resultAccountValidation.UsedGas, + PmValidationUsedGas: pmValidationUsedGas, + SenderValidAfter: validAfter, + SenderValidUntil: validUntil, + PmValidAfter: pmValidAfter, + PmValidUntil: pmValidUntil, + IsEOA: false, + } + + return vpr, nil +} + +func applyPaymasterValidationFrame(tx *types.Transaction, chainConfig *params.ChainConfig, signingHash common.Hash, evm *vm.EVM, gp *GasPool, statedb *state.StateDB, header *types.Header) ([]byte, uint64, uint64, uint64, error) { + /*** Paymaster Validation Frame ***/ + var pmValidationUsedGas uint64 + var paymasterContext []byte + var pmValidAfter uint64 + var pmValidUntil uint64 + paymasterMsg, err := preparePaymasterValidationMessage(tx, chainConfig, signingHash) + if err != nil { + return nil, 0, 0, 0, err + } + if paymasterMsg != nil { + resultPm, err := ApplyRip7560FrameMessage(evm, paymasterMsg, gp) + if err != nil { + return nil, 0, 0, 0, err + } + statedb.IntermediateRoot(true) + if resultPm.Failed() { + return nil, 0, 0, 0, errors.New("paymaster validation failed - invalid transaction") + } + pmValidationUsedGas = resultPm.UsedGas + paymasterContext, pmValidAfter, pmValidUntil, err = validatePaymasterReturnData(resultPm.ReturnData) + if err != nil { + return nil, 0, 0, 0, err + } + err = validateValidityTimeRange(header.Time, pmValidAfter, pmValidUntil) + if err != nil { + return nil, 0, 0, 0, err + } + } + return paymasterContext, pmValidationUsedGas, pmValidAfter, pmValidUntil, nil +} + +func applyPaymasterPostOpFrame(vpr *ValidationPhaseResult, executionResult *ExecutionResult, evm *vm.EVM, gp *GasPool, statedb *state.StateDB, header *types.Header) (*ExecutionResult, error) { + var paymasterPostOpResult *ExecutionResult + paymasterPostOpMsg, err := preparePostOpMessage(vpr, evm.ChainConfig(), executionResult) + if err != nil { + return nil, err + } + paymasterPostOpResult, err = ApplyRip7560FrameMessage(evm, paymasterPostOpMsg, gp) + if err != nil { + return nil, err + } + // TODO: revert the execution phase changes + return paymasterPostOpResult, nil +} + +func ApplyRip7560ExecutionPhase(config *params.ChainConfig, vpr *ValidationPhaseResult, bc ChainContext, author *common.Address, gp *GasPool, statedb *state.StateDB, header *types.Header, cfg vm.Config) (*types.Receipt, error) { + + // TODO: snapshot EVM - we will revert back here if postOp fails + + blockContext := NewEVMBlockContext(header, bc, author) + message, err := TransactionToMessage(vpr.Tx, types.MakeSigner(config, header.Number, header.Time), header.BaseFee) + txContext := NewEVMTxContext(message) + txContext.Origin = *vpr.Tx.Rip7560TransactionData().Sender + evm := vm.NewEVM(blockContext, txContext, statedb, config, cfg) + + var accountExecutionMsg *Message + if vpr.IsEOA { + accountExecutionMsg, err = prepareEOATargetExecutionMessage(vpr.Tx) + if err != nil { + return nil, err + } + } else { + accountExecutionMsg = prepareAccountExecutionMessage(vpr.Tx, evm.ChainConfig()) + } + executionResult, err := ApplyRip7560FrameMessage(evm, accountExecutionMsg, gp) + if err != nil { + return nil, err + } + root := statedb.IntermediateRoot(true).Bytes() + var paymasterPostOpResult *ExecutionResult + if len(vpr.PaymasterContext) != 0 { + paymasterPostOpResult, err = applyPaymasterPostOpFrame(vpr, executionResult, evm, gp, statedb, header) + root = statedb.IntermediateRoot(true).Bytes() + } + if err != nil { + return nil, err + } + + cumulativeGasUsed := + vpr.ValidationUsedGas + + vpr.DeploymentUsedGas + + vpr.PmValidationUsedGas + + executionResult.UsedGas + if paymasterPostOpResult != nil { + cumulativeGasUsed += + paymasterPostOpResult.UsedGas + } + + receipt := &types.Receipt{Type: vpr.Tx.Type(), PostState: root, CumulativeGasUsed: cumulativeGasUsed} + + // Set the receipt logs and create the bloom filter. + receipt.Logs = statedb.GetLogs(vpr.Tx.Hash(), header.Number.Uint64(), header.Hash()) + + if executionResult.Failed() || (paymasterPostOpResult != nil && paymasterPostOpResult.Failed()) { + receipt.Status = types.ReceiptStatusFailed + } else { + receipt.Status = types.ReceiptStatusSuccessful + } + return receipt, err +} + +func prepareNonceManagerMessage(baseTx *types.Transaction, chainConfig *params.ChainConfig) *Message { + tx := baseTx.Rip7560TransactionData() + key := make([]byte, 32) + fromBig, _ := uint256.FromBig(tx.BigNonce) + fromBig.WriteToSlice(key) + + nonceManagerData := make([]byte, 0) + nonceManagerData = append(nonceManagerData[:], tx.Sender.Bytes()...) + nonceManagerData = append(nonceManagerData[:], key...) + return &Message{ + From: chainConfig.EntryPointAddress, + To: &chainConfig.NonceManagerAddress, + Value: big.NewInt(0), + GasLimit: 100000, + GasPrice: tx.GasFeeCap, + GasFeeCap: tx.GasFeeCap, + GasTipCap: tx.GasTipCap, + Data: nonceManagerData, + AccessList: make(types.AccessList, 0), + SkipAccountChecks: true, + IsRip7560Frame: true, + } +} + +func prepareDeployerMessage(baseTx *types.Transaction, config *params.ChainConfig) *Message { + tx := baseTx.Rip7560TransactionData() + if len(tx.DeployerData) < 20 { + return nil + } + var deployerAddress common.Address = [20]byte(tx.DeployerData[0:20]) + return &Message{ + From: config.DeployerCallerAddress, + To: &deployerAddress, + Value: big.NewInt(0), + GasLimit: tx.ValidationGas, + GasPrice: tx.GasFeeCap, + GasFeeCap: tx.GasFeeCap, + GasTipCap: tx.GasTipCap, + Data: tx.DeployerData[20:], + AccessList: make(types.AccessList, 0), + SkipAccountChecks: true, + IsRip7560Frame: true, + } +} + +func prepareAccountValidationMessage(baseTx *types.Transaction, chainConfig *params.ChainConfig, signingHash common.Hash, deploymentUsedGas uint64) (*Message, error) { + tx := baseTx.Rip7560TransactionData() + jsondata := `[ + {"type":"function","name":"validateTransaction","inputs": [{"name": "version","type": "uint256"},{"name": "txHash","type": "bytes32"},{"name": "transaction","type": "bytes"}]} + ]` + + validateTransactionAbi, err := abi.JSON(strings.NewReader(jsondata)) + if err != nil { + return nil, err + } + txAbiEncoding, err := tx.AbiEncode() + validateTransactionData, err := validateTransactionAbi.Pack("validateTransaction", big.NewInt(0), signingHash, txAbiEncoding) + return &Message{ + From: chainConfig.EntryPointAddress, + To: tx.Sender, + Value: big.NewInt(0), + GasLimit: tx.ValidationGas - deploymentUsedGas, + GasPrice: tx.GasFeeCap, + GasFeeCap: tx.GasFeeCap, + GasTipCap: tx.GasTipCap, + Data: validateTransactionData, + AccessList: make(types.AccessList, 0), + SkipAccountChecks: true, + IsRip7560Frame: true, + }, nil +} + +func preparePaymasterValidationMessage(baseTx *types.Transaction, config *params.ChainConfig, signingHash common.Hash) (*Message, error) { + tx := baseTx.Rip7560TransactionData() + if len(tx.PaymasterData) < 20 { + return nil, nil + } + var paymasterAddress common.Address = [20]byte(tx.PaymasterData[0:20]) + jsondata := `[ + {"type":"function","name":"validatePaymasterTransaction","inputs": [{"name": "version","type": "uint256"},{"name": "txHash","type": "bytes32"},{"name": "transaction","type": "bytes"}]} + ]` + + validateTransactionAbi, err := abi.JSON(strings.NewReader(jsondata)) + txAbiEncoding, err := tx.AbiEncode() + data, err := validateTransactionAbi.Pack("validatePaymasterTransaction", big.NewInt(0), signingHash, txAbiEncoding) + + if err != nil { + return nil, err + } + return &Message{ + From: config.EntryPointAddress, + To: &paymasterAddress, + Value: big.NewInt(0), + GasLimit: tx.PaymasterGas, + GasPrice: tx.GasFeeCap, + GasFeeCap: tx.GasFeeCap, + GasTipCap: tx.GasTipCap, + Data: data, + AccessList: make(types.AccessList, 0), + SkipAccountChecks: true, + IsRip7560Frame: true, + }, nil +} + +func prepareAccountExecutionMessage(baseTx *types.Transaction, config *params.ChainConfig) *Message { + tx := baseTx.Rip7560TransactionData() + return &Message{ + From: config.EntryPointAddress, + To: tx.Sender, + Value: big.NewInt(0), + GasLimit: tx.Gas, + GasPrice: tx.GasFeeCap, + GasFeeCap: tx.GasFeeCap, + GasTipCap: tx.GasTipCap, + Data: tx.Data, + AccessList: make(types.AccessList, 0), + SkipAccountChecks: true, + IsRip7560Frame: true, + } +} + +func prepareEOATargetExecutionMessage(baseTx *types.Transaction) (*Message, error) { + tx := baseTx.Rip7560TransactionData() + if len(tx.Data) < 20 { + return nil, errors.New("RIP-7560 sent by an EOA but the transaction data is too short") + } + var to common.Address = [20]byte(tx.Data[0:20]) + return &Message{ + From: *tx.Sender, + To: &to, + Value: tx.Value, + GasLimit: tx.Gas, + GasPrice: tx.GasFeeCap, + GasFeeCap: tx.GasFeeCap, + GasTipCap: tx.GasTipCap, + Data: tx.Data[20:], + AccessList: make(types.AccessList, 0), + SkipAccountChecks: true, + IsRip7560Frame: true, + }, nil +} + +func preparePostOpMessage(vpr *ValidationPhaseResult, chainConfig *params.ChainConfig, executionResult *ExecutionResult) (*Message, error) { + if len(vpr.PaymasterContext) == 0 { + return nil, nil + } + + tx := vpr.Tx.Rip7560TransactionData() + jsondata := `[ + {"type":"function","name":"postPaymasterTransaction","inputs": [{"name": "success","type": "bool"},{"name": "actualGasCost","type": "uint256"},{"name": "context","type": "bytes"}]} + ]` + postPaymasterTransactionAbi, err := abi.JSON(strings.NewReader(jsondata)) + if err != nil { + return nil, err + } + postOpData, err := postPaymasterTransactionAbi.Pack("postPaymasterTransaction", true, big.NewInt(0), vpr.PaymasterContext) + if err != nil { + return nil, err + } + var paymasterAddress common.Address = [20]byte(tx.PaymasterData[0:20]) + return &Message{ + From: chainConfig.EntryPointAddress, + To: &paymasterAddress, + Value: big.NewInt(0), + GasLimit: tx.PaymasterGas - executionResult.UsedGas, + GasPrice: tx.GasFeeCap, + GasFeeCap: tx.GasFeeCap, + GasTipCap: tx.GasTipCap, + Data: postOpData, + AccessList: tx.AccessList, + SkipAccountChecks: true, + IsRip7560Frame: true, + }, nil +} + +func validateAccountReturnData(data []byte) (uint64, uint64, error) { + MAGIC_VALUE_SENDER := uint32(0xbf45c166) + if len(data) != 32 { + return 0, 0, errors.New("invalid account return data length") + } + magicExpected := binary.BigEndian.Uint32(data[:4]) + if magicExpected != MAGIC_VALUE_SENDER { + return 0, 0, errors.New("account did not return correct MAGIC_VALUE") + } + validAfter := binary.BigEndian.Uint64(data[4:12]) + validUntil := binary.BigEndian.Uint64(data[12:20]) + return validAfter, validUntil, nil +} + +func validatePaymasterReturnData(data []byte) ([]byte, uint64, uint64, error) { + MAGIC_VALUE_PAYMASTER := uint32(0xe0e6183a) + if len(data) < 4 { + return nil, 0, 0, errors.New("invalid paymaster return data length") + } + magicExpected := binary.BigEndian.Uint32(data[:4]) + if magicExpected != MAGIC_VALUE_PAYMASTER { + return nil, 0, 0, errors.New("paymaster did not return correct MAGIC_VALUE") + } + + jsondata := `[ + {"type":"function","name":"validatePaymasterTransaction","outputs": [{"name": "context","type": "bytes"},{"name": "validUntil","type": "uint256"},{"name": "validAfter","type": "uint256"}]} + ]` + validatePaymasterTransactionAbi, err := abi.JSON(strings.NewReader(jsondata)) + if err != nil { + // todo: wrap error message + return nil, 0, 0, err + } + decodedPmReturnData, err := validatePaymasterTransactionAbi.Unpack("validatePaymasterTransaction", data[4:]) + if err != nil { + return nil, 0, 0, err + } + context := decodedPmReturnData[0].([]byte) + validAfter := decodedPmReturnData[1].(*big.Int) + validUntil := decodedPmReturnData[2].(*big.Int) + return context, validAfter.Uint64(), validUntil.Uint64(), nil +} + +func validateValidityTimeRange(time uint64, validAfter uint64, validUntil uint64) error { + if validUntil == 0 && validAfter == 0 { + return nil + } + if validUntil < validAfter { + return errors.New("RIP-7560 transaction validity range invalid") + } + if time > validUntil { + return errors.New("RIP-7560 transaction validity expired") + } + if time < validAfter { + return errors.New("RIP-7560 transaction validity not reached yet") + } + return nil +} diff --git a/core/state_transition.go b/core/state_transition.go index a52e24dc43..70b642e747 100644 --- a/core/state_transition.go +++ b/core/state_transition.go @@ -146,6 +146,7 @@ type Message struct { // account nonce in state. It also disables checking that the sender is an EOA. // This field will be set to true for operations like RPC eth_call. SkipAccountChecks bool + IsRip7560Frame bool } // TransactionToMessage converts a transaction into a Message. @@ -213,6 +214,7 @@ type StateTransition struct { initialGas uint64 state vm.StateDB evm *vm.EVM + rip7560Frame bool } // NewStateTransition initialises and returns a new state transition object. diff --git a/core/txpool/blobpool/blobpool.go b/core/txpool/blobpool/blobpool.go index f1c2c10fc9..f1b965dedb 100644 --- a/core/txpool/blobpool/blobpool.go +++ b/core/txpool/blobpool/blobpool.go @@ -1653,3 +1653,18 @@ func (p *BlobPool) Status(hash common.Hash) txpool.TxStatus { } return txpool.TxStatusUnknown } + +func (pool *BlobPool) SubmitRip7560Bundle(_ *types.ExternallyReceivedBundle) error { + // nothing to do here + return nil +} + +func (pool *BlobPool) GetRip7560BundleStatus(_ common.Hash) (*types.BundleReceipt, error) { + // nothing to do here + return nil, nil +} + +func (pool *BlobPool) PendingRip7560Bundle() (*types.ExternallyReceivedBundle, error) { + // nothing to do here + return nil, nil +} diff --git a/core/txpool/legacypool/legacypool.go b/core/txpool/legacypool/legacypool.go index 4e1d26acf4..e5a59cf680 100644 --- a/core/txpool/legacypool/legacypool.go +++ b/core/txpool/legacypool/legacypool.go @@ -117,6 +117,8 @@ type BlockChain interface { // StateAt returns a state database for a given root hash (generally the head). StateAt(root common.Hash) (*state.StateDB, error) + + GetReceiptsByHash(hash common.Hash) types.Receipts } // Config are the configuration parameters of the transaction pool. @@ -1959,3 +1961,18 @@ func (t *lookup) RemotesBelowTip(threshold *big.Int) types.Transactions { func numSlots(tx *types.Transaction) int { return int((tx.Size() + txSlotSize - 1) / txSlotSize) } + +func (pool *LegacyPool) SubmitRip7560Bundle(_ *types.ExternallyReceivedBundle) error { + // nothing to do here + return nil +} + +func (pool *LegacyPool) GetRip7560BundleStatus(_ common.Hash) (*types.BundleReceipt, error) { + // nothing to do here + return nil, nil +} + +func (pool *LegacyPool) PendingRip7560Bundle() (*types.ExternallyReceivedBundle, error) { + // nothing to do here + return nil, nil +} diff --git a/core/txpool/rip7560pool/rip7560pool.go b/core/txpool/rip7560pool/rip7560pool.go new file mode 100644 index 0000000000..3c6670d34e --- /dev/null +++ b/core/txpool/rip7560pool/rip7560pool.go @@ -0,0 +1,273 @@ +package rip7560pool + +import ( + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core" + "github.com/ethereum/go-ethereum/core/txpool" + "github.com/ethereum/go-ethereum/core/txpool/legacypool" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" + "github.com/ethereum/go-ethereum/log" + "math/big" + "sync" + "sync/atomic" +) + +type Config struct { + MaxBundleSize uint + MaxBundleGas uint +} + +// Rip7560BundlerPool is the transaction pool dedicated to RIP-7560 AA transactions. +// This implementation relies on an external bundler process to perform most of the hard work. +type Rip7560BundlerPool struct { + config Config + chain legacypool.BlockChain + txFeed event.Feed + currentHead atomic.Pointer[types.Header] // Current head of the blockchain + + pendingBundles []*types.ExternallyReceivedBundle + includedBundles map[common.Hash]*types.BundleReceipt + + mu sync.Mutex + + coinbase common.Address +} + +func (pool *Rip7560BundlerPool) Init(_ uint64, head *types.Header, _ txpool.AddressReserver) error { + pool.pendingBundles = make([]*types.ExternallyReceivedBundle, 0) + pool.includedBundles = make(map[common.Hash]*types.BundleReceipt) + pool.currentHead.Store(head) + return nil +} + +func (pool *Rip7560BundlerPool) Close() error { + return nil +} + +func (pool *Rip7560BundlerPool) Reset(oldHead, newHead *types.Header) { + pool.mu.Lock() + defer pool.mu.Unlock() + + newIncludedBundles := pool.gatherIncludedBundlesStats(newHead) + for _, included := range newIncludedBundles { + pool.includedBundles[included.BundleHash] = included + } + + pendingBundles := make([]*types.ExternallyReceivedBundle, 0, len(pool.pendingBundles)) + for _, bundle := range pool.pendingBundles { + nextBlock := big.NewInt(0).Add(newHead.Number, big.NewInt(1)) + if bundle.ValidForBlock.Cmp(nextBlock) == 0 { + pendingBundles = append(pendingBundles, bundle) + } + } + pool.pendingBundles = pendingBundles + pool.currentHead.Store(newHead) +} + +// For simplicity, this function assumes 'Reset' called for each new block sequentially. +func (pool *Rip7560BundlerPool) gatherIncludedBundlesStats(newHead *types.Header) map[common.Hash]*types.BundleReceipt { + // 1. Is there a bundle included in the block? + + // note that in 'clique' mode Coinbase is always set to 0x000...000 + if newHead.Coinbase.Cmp(pool.coinbase) != 0 && newHead.Coinbase.Cmp(common.Address{}) != 0 { + // not our block + return nil + } + + // get all transaction hashes in block + add := pool.chain.GetBlock(newHead.Hash(), newHead.Number.Uint64()) + block := add.Transactions() + + receipts := pool.chain.GetReceiptsByHash(add.Hash()) + // match transactions in block to bundle ? + + includedBundles := make(map[common.Hash]*types.BundleReceipt) + + // 'pendingBundles' length is expected to be single digits, probably a single bundle in most cases + for _, bundle := range pool.pendingBundles { + if len(block) < len(bundle.Transactions) { + // this bundle does not even fit this block + continue + } + for i := 0; i < len(block); i++ { + transactions := make(types.Transactions, 0) + for j := 0; j < len(bundle.Transactions); j++ { + blockTx := block[i] + bundleTx := bundle.Transactions[j] + if bundleTx.Hash().Cmp(blockTx.Hash()) == 0 { + // tx hash has matched + transactions = append(transactions, blockTx) + if j == len(bundle.Transactions)-1 { + // FOUND BUNDLE IN BLOCK + receipt := createBundleReceipt(add, bundle.BundleHash, transactions, receipts) + includedBundles[bundle.BundleHash] = receipt + } else { + // let's see if next tx in bundle matches + i++ + } + } + } + } + + } + return includedBundles +} + +func createBundleReceipt(block *types.Block, BundleHash common.Hash, transactions types.Transactions, blockReceipts types.Receipts) *types.BundleReceipt { + receipts := make(types.Receipts, 0) + +OuterLoop: + for _, transaction := range transactions { + for _, receipt := range blockReceipts { + if receipt.TxHash == transaction.Hash() { + receipts = append(receipts, receipt) + continue OuterLoop + } + } + panic("receipt not found for transaction") + } + + var gasUsed uint64 = 0 + var gasPaidPriority = big.NewInt(0) + + for _, receipt := range receipts { + gasUsed += receipt.GasUsed + priorityFeePerGas := big.NewInt(0).Sub(receipt.EffectiveGasPrice, block.BaseFee()) + priorityFeePaid := big.NewInt(0).Mul(big.NewInt(int64(gasUsed)), priorityFeePerGas) + gasPaidPriority = big.NewInt(0).Add(gasPaidPriority, priorityFeePaid) + } + + return &types.BundleReceipt{ + BundleHash: BundleHash, + Count: uint64(len(transactions)), + Status: 0, + BlockNumber: block.NumberU64(), + BlockHash: block.Hash(), + TransactionReceipts: receipts, + GasUsed: gasUsed, + GasPaidPriority: gasPaidPriority, + BlockTimestamp: block.Time(), + } +} + +// SetGasTip is ignored by the External Bundler AA sub pool. +func (pool *Rip7560BundlerPool) SetGasTip(_ *big.Int) {} + +func (pool *Rip7560BundlerPool) Has(hash common.Hash) bool { + pool.mu.Lock() + defer pool.mu.Unlock() + + tx := pool.Get(hash) + return tx != nil +} + +func (pool *Rip7560BundlerPool) Get(hash common.Hash) *types.Transaction { + pool.mu.Lock() + defer pool.mu.Unlock() + + for _, bundle := range pool.pendingBundles { + for _, tx := range bundle.Transactions { + if tx.Hash().Cmp(hash) == 0 { + return tx + } + } + } + return nil +} + +func (pool *Rip7560BundlerPool) Add(_ []*types.Transaction, _ bool, _ bool) []error { + return nil +} + +func (pool *Rip7560BundlerPool) Pending(_ txpool.PendingFilter) map[common.Address][]*txpool.LazyTransaction { + return nil +} + +func (pool *Rip7560BundlerPool) PendingRip7560Bundle() (*types.ExternallyReceivedBundle, error) { + pool.mu.Lock() + defer pool.mu.Unlock() + + bundle := pool.selectExternalBundle() + return bundle, nil +} + +// SubscribeTransactions is not needed for the External Bundler AA sub pool and 'ch' will never be sent anything. +func (pool *Rip7560BundlerPool) SubscribeTransactions(ch chan<- core.NewTxsEvent, _ bool) event.Subscription { + return pool.txFeed.Subscribe(ch) +} + +// Nonce is only used from 'GetPoolNonce' which is not relevant for AA transactions. +func (pool *Rip7560BundlerPool) Nonce(_ common.Address) uint64 { + return 0 +} + +// Stats function not implemented for the External Bundler AA sub pool. +func (pool *Rip7560BundlerPool) Stats() (int, int) { + return 0, 0 +} + +// Content function not implemented for the External Bundler AA sub pool. +func (pool *Rip7560BundlerPool) Content() (map[common.Address][]*types.Transaction, map[common.Address][]*types.Transaction) { + return nil, nil +} + +// ContentFrom function not implemented for the External Bundler AA sub pool. +func (pool *Rip7560BundlerPool) ContentFrom(_ common.Address) ([]*types.Transaction, []*types.Transaction) { + return nil, nil +} + +// Locals are not necessary for AA Pool +func (pool *Rip7560BundlerPool) Locals() []common.Address { + return []common.Address{} +} + +func (pool *Rip7560BundlerPool) Status(_ common.Hash) txpool.TxStatus { + panic("implement me") +} + +// New creates a new RIP-7560 Account Abstraction Bundler transaction pool. +func New(config Config, chain legacypool.BlockChain, coinbase common.Address) *Rip7560BundlerPool { + return &Rip7560BundlerPool{ + config: config, + chain: chain, + coinbase: coinbase, + } +} + +// Filter rejects all individual transactions for External Bundler AA sub pool. +func (pool *Rip7560BundlerPool) Filter(_ *types.Transaction) bool { + return false +} + +func (pool *Rip7560BundlerPool) SubmitRip7560Bundle(bundle *types.ExternallyReceivedBundle) error { + pool.mu.Lock() + defer pool.mu.Unlock() + + currentBlock := pool.currentHead.Load().Number + nextBlock := big.NewInt(0).Add(currentBlock, big.NewInt(1)) + log.Error("RIP-7560 bundle submitted", "validForBlock", bundle.ValidForBlock.String(), "nextBlock", nextBlock.String()) + pool.pendingBundles = append(pool.pendingBundles, bundle) + if nextBlock.Cmp(bundle.ValidForBlock) == 0 { + pool.txFeed.Send(core.NewTxsEvent{Txs: bundle.Transactions}) + } + return nil +} + +func (pool *Rip7560BundlerPool) GetRip7560BundleStatus(hash common.Hash) (*types.BundleReceipt, error) { + pool.mu.Lock() + defer pool.mu.Unlock() + + return pool.includedBundles[hash], nil +} + +// Simply returns the bundle with the highest promised revenue by fully trusting the bundler-provided value. +func (pool *Rip7560BundlerPool) selectExternalBundle() *types.ExternallyReceivedBundle { + var selectedBundle *types.ExternallyReceivedBundle + for _, bundle := range pool.pendingBundles { + if selectedBundle == nil || selectedBundle.ExpectedRevenue.Cmp(bundle.ExpectedRevenue) == -1 { + selectedBundle = bundle + } + } + return selectedBundle +} diff --git a/core/txpool/subpool.go b/core/txpool/subpool.go index 9881ed1b8f..246a94bbeb 100644 --- a/core/txpool/subpool.go +++ b/core/txpool/subpool.go @@ -162,4 +162,10 @@ type SubPool interface { // Status returns the known status (unknown/pending/queued) of a transaction // identified by their hashes. Status(hash common.Hash) TxStatus + + // RIP-7560 specific subpool functions, other subpools should ignore these + + SubmitRip7560Bundle(bundle *types.ExternallyReceivedBundle) error + GetRip7560BundleStatus(hash common.Hash) (*types.BundleReceipt, error) + PendingRip7560Bundle() (*types.ExternallyReceivedBundle, error) } diff --git a/core/txpool/txpool_rip7560.go b/core/txpool/txpool_rip7560.go new file mode 100644 index 0000000000..5e5ef8f8e7 --- /dev/null +++ b/core/txpool/txpool_rip7560.go @@ -0,0 +1,46 @@ +package txpool + +import ( + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" +) + +// SubmitBundle inserts the entire bundle of Type 4 transactions into the relevant pool. +func (p *TxPool) SubmitRip7560Bundle(bundle *types.ExternallyReceivedBundle) error { + // todo: we cannot 'filter-out' the AA pool so just passing to all pools - only AA pool has code in SubmitBundle + for _, subpool := range p.subpools { + err := subpool.SubmitRip7560Bundle(bundle) + if err != nil { + return err + } + } + return nil +} + +func (p *TxPool) GetRip7560BundleStatus(hash common.Hash) (*types.BundleReceipt, error) { + // todo: we cannot 'filter-out' the AA pool so just passing to all pools - only AA pool has code in SubmitBundle + for _, subpool := range p.subpools { + bundleStats, err := subpool.GetRip7560BundleStatus(hash) + if err != nil { + return nil, err + } + if bundleStats != nil { + return bundleStats, nil + } + } + return nil, nil +} + +func (p *TxPool) PendingRip7560Bundle() (*types.ExternallyReceivedBundle, error) { + // todo: we cannot 'filter-out' the AA pool so just passing to all pools - only AA pool has code in PendingBundle + for _, subpool := range p.subpools { + pendingBundle, err := subpool.PendingRip7560Bundle() + if err != nil { + return nil, err + } + if pendingBundle != nil { + return pendingBundle, nil + } + } + return nil, nil +} diff --git a/core/types/transaction.go b/core/types/transaction.go index 6a27ecbfec..e8b389d8e6 100644 --- a/core/types/transaction.go +++ b/core/types/transaction.go @@ -49,6 +49,7 @@ const ( AccessListTxType = 0x01 DynamicFeeTxType = 0x02 BlobTxType = 0x03 + Rip7560Type = 0x04 ) // Transaction is an Ethereum transaction. @@ -466,6 +467,12 @@ func (tx *Transaction) WithBlobTxSidecar(sideCar *BlobTxSidecar) *Transaction { return cpy } +func (tx *Transaction) Rip7560TransactionData() *Rip7560AccountAbstractionTx { + inner := tx.inner + ptr := inner.(*Rip7560AccountAbstractionTx) + return ptr +} + // SetTime sets the decoding time of a transaction. This is used by tests to set // arbitrary times and by persistent transaction pools when loading old txs from // disk. diff --git a/core/types/tx_rip7560.go b/core/types/tx_rip7560.go new file mode 100644 index 0000000000..b63de28dc9 --- /dev/null +++ b/core/types/tx_rip7560.go @@ -0,0 +1,221 @@ +// Copyright 2021 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package types + +import ( + "bytes" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/rlp" + "math/big" +) + +const ScaTransactionSubtype = 0x01 + +// Rip7560AccountAbstractionTx represents an RIP-7560 transaction. +type Rip7560AccountAbstractionTx struct { + Subtype byte + // overlapping fields + ChainID *big.Int + GasTipCap *big.Int // a.k.a. maxPriorityFeePerGas + GasFeeCap *big.Int // a.k.a. maxFeePerGas + Gas uint64 + Data []byte + AccessList AccessList + + // extra fields + Sender *common.Address + Signature []byte + PaymasterData []byte + DeployerData []byte + BuilderFee *big.Int + ValidationGas uint64 + PaymasterGas uint64 + PostOpGas uint64 + BigNonce *big.Int + + // removed fields + To *common.Address + Nonce uint64 + Value *big.Int +} + +// copy creates a deep copy of the transaction data and initializes all fields. +func (tx *Rip7560AccountAbstractionTx) copy() TxData { + cpy := &Rip7560AccountAbstractionTx{ + Subtype: tx.Subtype, + To: copyAddressPtr(tx.To), + Data: common.CopyBytes(tx.Data), + Gas: tx.Gas, + // These are copied below. + AccessList: make(AccessList, len(tx.AccessList)), + Value: new(big.Int), + ChainID: new(big.Int), + GasTipCap: new(big.Int), + GasFeeCap: new(big.Int), + + BigNonce: new(big.Int), + Sender: copyAddressPtr(tx.Sender), + Signature: common.CopyBytes(tx.Signature), + PaymasterData: common.CopyBytes(tx.PaymasterData), + DeployerData: common.CopyBytes(tx.DeployerData), + BuilderFee: new(big.Int), + ValidationGas: tx.ValidationGas, + PaymasterGas: tx.PaymasterGas, + } + copy(cpy.AccessList, tx.AccessList) + if tx.Value != nil { + cpy.Value.Set(tx.Value) + } + if tx.ChainID != nil { + cpy.ChainID.Set(tx.ChainID) + } + if tx.GasTipCap != nil { + cpy.GasTipCap.Set(tx.GasTipCap) + } + if tx.GasFeeCap != nil { + cpy.GasFeeCap.Set(tx.GasFeeCap) + } + if tx.BigNonce != nil { + cpy.BigNonce.Set(tx.BigNonce) + } + if tx.BuilderFee != nil { + cpy.BuilderFee.Set(tx.BuilderFee) + } + return cpy +} + +// accessors for innerTx. +func (tx *Rip7560AccountAbstractionTx) txType() byte { return Rip7560Type } +func (tx *Rip7560AccountAbstractionTx) chainID() *big.Int { return tx.ChainID } +func (tx *Rip7560AccountAbstractionTx) accessList() AccessList { return tx.AccessList } +func (tx *Rip7560AccountAbstractionTx) data() []byte { return tx.Data } +func (tx *Rip7560AccountAbstractionTx) gas() uint64 { return tx.Gas } +func (tx *Rip7560AccountAbstractionTx) gasFeeCap() *big.Int { return tx.GasFeeCap } +func (tx *Rip7560AccountAbstractionTx) gasTipCap() *big.Int { return tx.GasTipCap } +func (tx *Rip7560AccountAbstractionTx) gasPrice() *big.Int { return tx.GasFeeCap } +func (tx *Rip7560AccountAbstractionTx) value() *big.Int { return tx.Value } +func (tx *Rip7560AccountAbstractionTx) nonce() uint64 { return 0 } +func (tx *Rip7560AccountAbstractionTx) bigNonce() *big.Int { return tx.BigNonce } +func (tx *Rip7560AccountAbstractionTx) to() *common.Address { return tx.To } + +func (tx *Rip7560AccountAbstractionTx) effectiveGasPrice(dst *big.Int, baseFee *big.Int) *big.Int { + if baseFee == nil { + return dst.Set(tx.GasFeeCap) + } + tip := dst.Sub(tx.GasFeeCap, baseFee) + if tip.Cmp(tx.GasTipCap) > 0 { + tip.Set(tx.GasTipCap) + } + return tip.Add(tip, baseFee) +} + +func (tx *Rip7560AccountAbstractionTx) rawSignatureValues() (v, r, s *big.Int) { + return new(big.Int), new(big.Int), new(big.Int) +} + +func (tx *Rip7560AccountAbstractionTx) setSignatureValues(chainID, v, r, s *big.Int) { + //tx.ChainID, tx.V, tx.R, tx.S = chainID, v, r, s +} + +// encode the subtype byte and the payload-bearing bytes of the RIP-7560 transaction +func (tx *Rip7560AccountAbstractionTx) encode(b *bytes.Buffer) error { + b.WriteByte(ScaTransactionSubtype) + return rlp.Encode(b, tx) +} + +// decode the payload-bearing bytes of the encoded RIP-7560 transaction payload +func (tx *Rip7560AccountAbstractionTx) decode(input []byte) error { + tx.Subtype = ScaTransactionSubtype + return rlp.DecodeBytes(input[1:], tx) +} + +// Rip7560Transaction an equivalent of a solidity struct only used to encode the 'transaction' parameter +type Rip7560Transaction struct { + Sender common.Address + Nonce *big.Int + ValidationGasLimit *big.Int + PaymasterGasLimit *big.Int + CallGasLimit *big.Int + MaxFeePerGas *big.Int + MaxPriorityFeePerGas *big.Int + BuilderFee *big.Int + PaymasterData []byte + DeployerData []byte + CallData []byte + Signature []byte +} + +func (tx *Rip7560AccountAbstractionTx) AbiEncode() ([]byte, error) { + structThing, _ := abi.NewType("tuple", "struct thing", []abi.ArgumentMarshaling{ + {Name: "sender", Type: "address"}, + {Name: "nonce", Type: "uint256"}, + {Name: "validationGasLimit", Type: "uint256"}, + {Name: "paymasterGasLimit", Type: "uint256"}, + {Name: "callGasLimit", Type: "uint256"}, + {Name: "maxFeePerGas", Type: "uint256"}, + {Name: "maxPriorityFeePerGas", Type: "uint256"}, + {Name: "builderFee", Type: "uint256"}, + {Name: "paymasterData", Type: "bytes"}, + {Name: "deployerData", Type: "bytes"}, + {Name: "callData", Type: "bytes"}, + {Name: "signature", Type: "bytes"}, + }) + + args := abi.Arguments{ + {Type: structThing, Name: "param_one"}, + } + record := &Rip7560Transaction{ + Sender: *tx.Sender, + Nonce: tx.BigNonce, + ValidationGasLimit: big.NewInt(int64(tx.ValidationGas)), + PaymasterGasLimit: big.NewInt(int64(tx.PaymasterGas)), + CallGasLimit: big.NewInt(int64(tx.Gas)), + MaxFeePerGas: tx.GasFeeCap, + MaxPriorityFeePerGas: tx.GasTipCap, + BuilderFee: tx.BuilderFee, + PaymasterData: tx.PaymasterData, + DeployerData: tx.DeployerData, + CallData: tx.Data, + Signature: tx.Signature, + } + packed, err := args.Pack(&record) + return packed, err +} + +// ExternallyReceivedBundle represents a bundle of Type 4 transactions received from a trusted 3rd party. +// The validator includes the bundle in the original order atomically or drops it completely. +type ExternallyReceivedBundle struct { + BundlerId string + BundleHash common.Hash + ExpectedRevenue *big.Int + ValidForBlock *big.Int + Transactions []*Transaction +} + +// BundleReceipt represents a receipt for an ExternallyReceivedBundle successfully included in a block. +type BundleReceipt struct { + BundleHash common.Hash + Count uint64 + Status uint64 // 0=included / 1=pending / 2=invalid / 3=unknown + BlockNumber uint64 + BlockHash common.Hash + TransactionReceipts []*Receipt + GasUsed uint64 + GasPaidPriority *big.Int + BlockTimestamp uint64 +} diff --git a/eth/api_backend_rip7560.go b/eth/api_backend_rip7560.go new file mode 100644 index 0000000000..4a8ad2f36e --- /dev/null +++ b/eth/api_backend_rip7560.go @@ -0,0 +1,15 @@ +package eth + +import ( + "context" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" +) + +func (b *EthAPIBackend) SubmitRip7560Bundle(bundle *types.ExternallyReceivedBundle) error { + return b.eth.txPool.SubmitRip7560Bundle(bundle) +} + +func (b *EthAPIBackend) GetRip7560BundleStatus(ctx context.Context, hash common.Hash) (*types.BundleReceipt, error) { + return b.eth.txPool.GetRip7560BundleStatus(hash) +} diff --git a/eth/backend.go b/eth/backend.go index e616b5f2f1..0c2f829e53 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -21,6 +21,7 @@ import ( "encoding/json" "errors" "fmt" + "github.com/ethereum/go-ethereum/core/txpool/rip7560pool" "math/big" "runtime" "sync" @@ -240,7 +241,13 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { } legacyPool := legacypool.New(config.TxPool, eth.blockchain) - eth.txPool, err = txpool.New(config.TxPool.PriceLimit, eth.blockchain, []txpool.SubPool{legacyPool, blobPool}) + rip7560PoolConfig := rip7560pool.Config{ + MaxBundleGas: 10000000, + MaxBundleSize: 100, + } + rip7560 := rip7560pool.New(rip7560PoolConfig, eth.blockchain, config.Miner.Etherbase) + + eth.txPool, err = txpool.New(config.TxPool.PriceLimit, eth.blockchain, []txpool.SubPool{legacyPool, blobPool, rip7560}) if err != nil { return nil, err } diff --git a/internal/ethapi/backend.go b/internal/ethapi/backend.go index 2a45ba0921..35c4a90419 100644 --- a/internal/ethapi/backend.go +++ b/internal/ethapi/backend.go @@ -97,6 +97,10 @@ type Backend interface { SubscribeLogsEvent(ch chan<- []*types.Log) event.Subscription BloomStatus() (uint64, uint64) ServiceFilter(ctx context.Context, session *bloombits.MatcherSession) + + // RIP-7560 specific functions + SubmitRip7560Bundle(bundle *types.ExternallyReceivedBundle) error + GetRip7560BundleStatus(ctx context.Context, hash common.Hash) (*types.BundleReceipt, error) } func GetAPIs(apiBackend Backend) []rpc.API { diff --git a/internal/ethapi/rip7560api.go b/internal/ethapi/rip7560api.go new file mode 100644 index 0000000000..0245b32077 --- /dev/null +++ b/internal/ethapi/rip7560api.go @@ -0,0 +1,62 @@ +package ethapi + +import ( + "context" + "errors" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/rlp" + "golang.org/x/crypto/sha3" + "math/big" +) + +func (s *TransactionAPI) SendRip7560TransactionsBundle(ctx context.Context, args []TransactionArgs, creationBlock *big.Int, expectedRevenue *big.Int, bundlerId string) (common.Hash, error) { + if len(args) == 0 { + return common.Hash{}, errors.New("submitted bundle has zero length") + } + txs := make([]*types.Transaction, len(args)) + for i := 0; i < len(args); i++ { + txs[i] = args[i].ToTransaction() + } + bundle := &types.ExternallyReceivedBundle{ + BundlerId: bundlerId, + ExpectedRevenue: expectedRevenue, + ValidForBlock: creationBlock, + Transactions: txs, + } + bundleHash := calculateBundleHash(txs) + bundle.BundleHash = bundleHash + err := SubmitRip7560Bundle(ctx, s.b, bundle) + if err != nil { + return common.Hash{}, err + } + return bundleHash, nil +} + +func (s *TransactionAPI) GetRip7560BundleStatus(ctx context.Context, hash common.Hash) (*types.BundleReceipt, error) { + bundleStats, err := s.b.GetRip7560BundleStatus(ctx, hash) + return bundleStats, err +} + +// TODO: If this code is indeed necessary, keep it in utils; better - remove altogether. +func calculateBundleHash(txs []*types.Transaction) common.Hash { + appendedTxIds := make([]byte, 0) + for _, tx := range txs { + txHash := tx.Hash() + appendedTxIds = append(appendedTxIds, txHash[:]...) + } + + return rlpHash(appendedTxIds) +} + +func rlpHash(x interface{}) (h common.Hash) { + hw := sha3.NewLegacyKeccak256() + rlp.Encode(hw, x) + hw.Sum(h[:0]) + return h +} + +// SubmitRip7560Bundle is a helper function that submits a bundle of Type 4 transactions to txPool and logs a message. +func SubmitRip7560Bundle(ctx context.Context, b Backend, bundle *types.ExternallyReceivedBundle) error { + return b.SubmitRip7560Bundle(bundle) +} diff --git a/miner/worker.go b/miner/worker.go index 5dc3e2056b..c52a9c7eb0 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -373,6 +373,22 @@ func (miner *Miner) commitTransactions(env *environment, plainTxs, blobTxs *tran return nil } +func (miner *Miner) commitRip7560TransactionsBundle(env *environment, txs *types.ExternallyReceivedBundle, _ *atomic.Int32) error { + + // todo: copied over to fix crash, probably should do it once + gasLimit := env.header.GasLimit + if env.gasPool == nil { + env.gasPool = new(core.GasPool).AddGas(gasLimit) + } + + validatedTxs, receipts, _, err := core.HandleRip7560Transactions(txs.Transactions, 0, env.state, &env.coinbase, env.header, env.gasPool, miner.chainConfig, miner.chain, vm.Config{}) + + env.txs = append(env.txs, validatedTxs...) + env.receipts = append(env.receipts, receipts...) + env.tcount += len(validatedTxs) + return err +} + // fillTransactions retrieves the pending transactions from the txpool and fills them // into the given sealing block. The transaction selection and ordering strategy can // be customized with the plugin in the future. @@ -411,6 +427,14 @@ func (miner *Miner) fillTransactions(interrupt *atomic.Int32, env *environment) localBlobTxs[account] = txs } } + + pendingBundle, err := miner.txpool.PendingRip7560Bundle() + if pendingBundle != nil { + if err = miner.commitRip7560TransactionsBundle(env, pendingBundle, interrupt); err != nil { + return err + } + } + // Fill the block with all available pending transactions. if len(localPlainTxs) > 0 || len(localBlobTxs) > 0 { plainTxs := newTransactionsByPriceAndNonce(env.signer, localPlainTxs, env.header.BaseFee) diff --git a/params/config.go b/params/config.go index 486cb6d132..9d9fc242bc 100644 --- a/params/config.go +++ b/params/config.go @@ -368,6 +368,11 @@ type ChainConfig struct { // Various consensus engines Ethash *EthashConfig `json:"ethash,omitempty"` Clique *CliqueConfig `json:"clique,omitempty"` + + // RIP-7560 specific config parameters + EntryPointAddress common.Address `json:"entryPointAddress,omitempty"` + NonceManagerAddress common.Address `json:"nonceManagerAddress,omitempty"` + DeployerCallerAddress common.Address `json:"deployerCallerAddress,omitempty"` } // EthashConfig is the consensus engine configs for proof-of-work based sealing.