diff --git a/accounts/external/backend.go b/accounts/external/backend.go index d403b7e562..b19a7634fd 100644 --- a/accounts/external/backend.go +++ b/accounts/external/backend.go @@ -217,6 +217,11 @@ func (api *ExternalSigner) SignTx(account accounts.Account, tx *types.Transactio case types.DynamicFeeTxType: args.MaxFeePerGas = (*hexutil.Big)(tx.GasFeeCap()) args.MaxPriorityFeePerGas = (*hexutil.Big)(tx.GasTipCap()) + case types.ArbitrumSubtypedTxType: + if types.GetArbitrumTxSubtype(tx) == types.ArbitrumTippingTxSubtype { + args.MaxFeePerGas = (*hexutil.Big)(tx.GasFeeCap()) + args.MaxPriorityFeePerGas = (*hexutil.Big)(tx.GasTipCap()) + } default: return nil, fmt.Errorf("unsupported tx type %d", tx.Type()) } diff --git a/core/types/arb_types.go b/core/types/arb_types.go index 84be0cbc57..6e2553b5a4 100644 --- a/core/types/arb_types.go +++ b/core/types/arb_types.go @@ -3,6 +3,7 @@ package types import ( "context" "encoding/binary" + "errors" "fmt" "math/big" @@ -13,6 +14,12 @@ import ( "github.com/ethereum/go-ethereum/common" ) +const ( + arbitrumSubtypeOffset = 0xff + ArbitrumInvalidSubtype = 0 + ArbitrumTippingTxSubtype = 1 +) + type fallbackError struct { } @@ -460,3 +467,54 @@ func DeserializeHeaderExtraInformation(header *Header) (HeaderInfo, error) { extra.ArbOSFormatVersion = binary.BigEndian.Uint64(header.MixDigest[16:24]) return extra, nil } + +type ArbitrumSubtypedTx struct { + TxData +} + +func (tx *ArbitrumSubtypedTx) copy() TxData { + return &ArbitrumSubtypedTx{ + TxData: tx.TxData.copy(), + } +} + +func (tx *ArbitrumSubtypedTx) txType() byte { return ArbitrumSubtypedTxType } +func (tx *ArbitrumSubtypedTx) TxSubtype() byte { return tx.TxData.txType() } + +func signSubtypedLikeDynamicFeeTx(tx *Transaction) bool { + return GetArbitrumTxSubtype(tx) == ArbitrumTippingTxSubtype +} + +func GetArbitrumTxSubtype(tx *Transaction) byte { + switch inner := tx.inner.(type) { + case *ArbitrumSubtypedTx: + return inner.TxSubtype() + default: + return ArbitrumInvalidSubtype + } +} + +type ArbitrumTippingTx struct { + DynamicFeeTx `rlp:"flat"` +} + +func NewArbitrumTippingTx(origTx *Transaction) (*Transaction, error) { + dynamicPtr, ok := origTx.GetInner().(*DynamicFeeTx) + if origTx.Type() != DynamicFeeTxType || !ok { + return nil, errors.New("attempt to arbitrum-wrap into tipping transaction a transaction that is not a dynamic fee transaction") + } + inner := ArbitrumSubtypedTx{ + TxData: &ArbitrumTippingTx{ + DynamicFeeTx: *dynamicPtr, + }} + return NewTx(&inner), nil +} + +func (tx *ArbitrumTippingTx) copy() TxData { + dynamicCopy := tx.DynamicFeeTx.copy().(*DynamicFeeTx) + return &ArbitrumTippingTx{ + DynamicFeeTx: *dynamicCopy, + } +} + +func (tx *ArbitrumTippingTx) txType() byte { return ArbitrumTippingTxSubtype } diff --git a/core/types/receipt.go b/core/types/receipt.go index 8b35aefe81..454468e773 100644 --- a/core/types/receipt.go +++ b/core/types/receipt.go @@ -73,6 +73,8 @@ type Receipt struct { BlockHash common.Hash `json:"blockHash,omitempty"` BlockNumber *big.Int `json:"blockNumber,omitempty"` TransactionIndex uint `json:"transactionIndex"` + + Subtype uint8 `json:"type,omitempty"` } type receiptMarshaling struct { @@ -171,6 +173,9 @@ func (r *Receipt) EncodeRLP(w io.Writer) error { // encodeTyped writes the canonical encoding of a typed receipt to w. func (r *Receipt) encodeTyped(data *receiptRLP, w *bytes.Buffer) error { w.WriteByte(r.Type) + if r.Type == ArbitrumSubtypedTxType { + w.WriteByte(r.Subtype) + } return rlp.Encode(w, data) } @@ -241,6 +246,19 @@ func (r *Receipt) decodeTyped(b []byte) error { } r.Type = b[0] return r.setFromRLP(data) + case ArbitrumSubtypedTxType: + if len(b) <= 2 { + return errShortTypedReceipt + } + var data receiptRLP + err := rlp.DecodeBytes(b[2:], &data) + if err != nil { + return err + } + r.Type = b[0] + r.Subtype = b[1] + return r.setFromRLP(data) + default: return ErrTxTypeNotSupported } diff --git a/core/types/transaction.go b/core/types/transaction.go index 5debed4493..76509363c7 100644 --- a/core/types/transaction.go +++ b/core/types/transaction.go @@ -45,6 +45,7 @@ const ( LegacyTxType = iota AccessListTxType DynamicFeeTxType + ArbitrumSubtypedTxType = 99 ArbitrumDepositTxType = 100 ArbitrumUnsignedTxType = 101 ArbitrumContractTxType = 102 @@ -116,6 +117,11 @@ func (tx *Transaction) EncodeRLP(w io.Writer) error { // encodeTyped writes the canonical encoding of a typed transaction to w. func (tx *Transaction) encodeTyped(w *bytes.Buffer) error { + if tx.Type() == ArbitrumSubtypedTxType { + w.WriteByte(tx.Type()) + w.WriteByte(tx.inner.(*ArbitrumSubtypedTx).TxSubtype()) + return rlp.Encode(w, tx.inner.(*ArbitrumSubtypedTx).TxData) + } w.WriteByte(tx.Type()) return rlp.Encode(w, tx.inner) } @@ -187,6 +193,22 @@ func (tx *Transaction) decodeTyped(b []byte, arbParsing bool) (TxData, error) { if len(b) <= 1 { return nil, errShortTypedTx } + txType := uint64(b[0]) + if txType == ArbitrumSubtypedTxType { + if len(b) <= 2 { + return nil, errShortTypedTx + } + var inner ArbitrumSubtypedTx + switch b[1] { + case ArbitrumTippingTxSubtype: + var tipping ArbitrumTippingTx + err := rlp.DecodeBytes(b[2:], &tipping) + inner.TxData = &tipping + return &inner, err + default: + return nil, ErrTxTypeNotSupported + } + } if arbParsing { switch b[0] { case ArbitrumDepositTxType: diff --git a/core/types/transaction_marshalling.go b/core/types/transaction_marshalling.go index a5b42f3faf..9a42a55254 100644 --- a/core/types/transaction_marshalling.go +++ b/core/types/transaction_marshalling.go @@ -47,6 +47,7 @@ type txJSON struct { AccessList *AccessList `json:"accessList,omitempty"` // Arbitrum fields: + Subtype *hexutil.Uint64 `json:"subtype,omitempty"` // ArbitrumSubtypedTx From *common.Address `json:"from,omitempty"` // Contract SubmitRetryable Unsigned Retry RequestId *common.Hash `json:"requestId,omitempty"` // Contract SubmitRetryable Deposit TicketId *common.Hash `json:"ticketId,omitempty"` // Retry @@ -192,6 +193,29 @@ func (t *Transaction) MarshalJSON() ([]byte, error) { data := tx.data() enc.Data = (*hexutil.Bytes)(&data) enc.To = t.To() + case *ArbitrumSubtypedTx: + subtype := uint64(tx.TxSubtype()) + enc.Subtype = (*hexutil.Uint64)(&subtype) + switch subtype { + case ArbitrumTippingTxSubtype: + enc.ChainID = (*hexutil.Big)(tx.chainID()) + accessList := tx.accessList() + enc.AccessList = &accessList + nonce := tx.nonce() + enc.Nonce = (*hexutil.Uint64)(&nonce) + gas := tx.gas() + enc.Gas = (*hexutil.Uint64)(&gas) + enc.MaxFeePerGas = (*hexutil.Big)(tx.gasFeeCap()) + enc.MaxPriorityFeePerGas = (*hexutil.Big)(tx.gasTipCap()) + enc.Value = (*hexutil.Big)(tx.value()) + data := tx.data() + enc.Data = (*hexutil.Bytes)(&data) + enc.To = t.To() + v, r, s := tx.rawSignatureValues() + enc.V = (*hexutil.Big)(v) + enc.R = (*hexutil.Big)(r) + enc.S = (*hexutil.Big)(s) + } } return json.Marshal(&enc) } @@ -205,7 +229,15 @@ func (t *Transaction) UnmarshalJSON(input []byte) error { // Decode / verify fields according to transaction type. var inner TxData - switch dec.Type { + decType := uint64(dec.Type) + if decType == ArbitrumSubtypedTxType { + if dec.Subtype != nil { + decType = uint64(*dec.Subtype) + arbitrumSubtypeOffset + } else { + return errors.New("missing required field 'subtype' in transaction") + } + } + switch decType { case LegacyTxType: var itx LegacyTx inner = &itx @@ -304,9 +336,8 @@ func (t *Transaction) UnmarshalJSON(input []byte) error { } } - case DynamicFeeTxType: + case DynamicFeeTxType, ArbitrumTippingTxSubtype + arbitrumSubtypeOffset: var itx DynamicFeeTx - inner = &itx // Access list is optional for now. if dec.AccessList != nil { itx.AccessList = *dec.AccessList @@ -360,6 +391,15 @@ func (t *Transaction) UnmarshalJSON(input []byte) error { return err } } + if decType == ArbitrumTippingTxSubtype+arbitrumSubtypeOffset { + inner = &ArbitrumSubtypedTx{ + TxData: &ArbitrumTippingTx{ + DynamicFeeTx: itx, + }, + } + } else { + inner = &itx + } case ArbitrumLegacyTxType: var itx LegacyTx diff --git a/core/types/transaction_signing.go b/core/types/transaction_signing.go index ad12fa98c8..567ac18466 100644 --- a/core/types/transaction_signing.go +++ b/core/types/transaction_signing.go @@ -193,7 +193,7 @@ func NewLondonSigner(chainId *big.Int) Signer { } func (s londonSigner) Sender(tx *Transaction) (common.Address, error) { - if tx.Type() != DynamicFeeTxType { + if tx.Type() != DynamicFeeTxType && !signSubtypedLikeDynamicFeeTx(tx) { return s.eip2930Signer.Sender(tx) } V, R, S := tx.RawSignatureValues() @@ -212,13 +212,18 @@ func (s londonSigner) Equal(s2 Signer) bool { } func (s londonSigner) SignatureValues(tx *Transaction, sig []byte) (R, S, V *big.Int, err error) { - txdata, ok := tx.inner.(*DynamicFeeTx) + var txdata TxData + var ok bool + txdata, ok = tx.inner.(*DynamicFeeTx) if !ok { - return s.eip2930Signer.SignatureValues(tx, sig) + txdata, ok = tx.inner.(*ArbitrumSubtypedTx) + if !ok || !signSubtypedLikeDynamicFeeTx(tx) { + return s.eip2930Signer.SignatureValues(tx, sig) + } } // Check that chain ID of tx matches the signer. We also accept ID zero here, // because it indicates that the chain ID was not specified in the tx. - if txdata.ChainID.Sign() != 0 && txdata.ChainID.Cmp(s.chainId) != 0 { + if txdata.chainID().Sign() != 0 && txdata.chainID().Cmp(s.chainId) != 0 { return nil, nil, nil, ErrInvalidChainId } R, S, _ = decodeSignature(sig) @@ -229,6 +234,22 @@ func (s londonSigner) SignatureValues(tx *Transaction, sig []byte) (R, S, V *big // Hash returns the hash to be signed by the sender. // It does not uniquely identify the transaction. func (s londonSigner) Hash(tx *Transaction) common.Hash { + if signSubtypedLikeDynamicFeeTx(tx) { + return prefixedRlpHash( + tx.Type(), + []interface{}{ + tx.inner.(*ArbitrumSubtypedTx).TxSubtype(), + s.chainId, + tx.Nonce(), + tx.GasTipCap(), + tx.GasFeeCap(), + tx.Gas(), + tx.To(), + tx.Value(), + tx.Data(), + tx.AccessList(), + }) + } if tx.Type() != DynamicFeeTxType { return s.eip2930Signer.Hash(tx) } diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index 842da79bcd..326d44ef7c 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -1406,6 +1406,7 @@ type RPCTransaction struct { S *hexutil.Big `json:"s"` // Arbitrum fields: + Subtype hexutil.Uint64 `json:"subtype",omitempty` // ArbiturumSubtypedTx RequestId *common.Hash `json:"requestId,omitempty"` // Contract SubmitRetryable Deposit TicketId *common.Hash `json:"ticketId,omitempty"` // Retry MaxRefund *hexutil.Big `json:"maxRefund,omitempty"` // Retry @@ -1501,6 +1502,26 @@ func newRPCTransaction(tx *types.Transaction, blockHash common.Hash, blockNumber result.MaxSubmissionFee = (*hexutil.Big)(inner.MaxSubmissionFee) result.GasFeeCap = (*hexutil.Big)(inner.GasFeeCap) result.ChainID = (*hexutil.Big)(inner.ChainId) + case *types.ArbitrumSubtypedTx: + subtype := inner.TxSubtype() + result.Subtype = hexutil.Uint64(subtype) + switch subtype { + case types.ArbitrumTippingTxSubtype: + al := tx.AccessList() + result.Accesses = &al + result.ChainID = (*hexutil.Big)(tx.ChainId()) + result.GasFeeCap = (*hexutil.Big)(tx.GasFeeCap()) + result.GasTipCap = (*hexutil.Big)(tx.GasTipCap()) + // if the transaction has been mined, compute the effective gas price + if baseFee != nil && blockHash != (common.Hash{}) { + // price = min(tip, gasFeeCap - baseFee) + baseFee + price := math.BigMin(new(big.Int).Add(tx.GasTipCap(), baseFee), tx.GasFeeCap()) + result.GasPrice = (*hexutil.Big)(price) + } else { + result.GasPrice = (*hexutil.Big)(tx.GasFeeCap()) + } + } + } return result } diff --git a/rlp/decode.go b/rlp/decode.go index 9214dbfb37..29c701723c 100644 --- a/rlp/decode.go +++ b/rlp/decode.go @@ -172,7 +172,7 @@ func makeDecoder(typ reflect.Type, tags rlpstruct.Tags) (dec decoder, err error) case kind == reflect.Slice || kind == reflect.Array: return makeListDecoder(typ, tags) case kind == reflect.Struct: - return makeStructDecoder(typ) + return makeStructDecoder(typ, tags.Flat) case kind == reflect.Interface: return decodeInterface, nil default: @@ -376,7 +376,7 @@ func decodeByteArray(s *Stream, val reflect.Value) error { return nil } -func makeStructDecoder(typ reflect.Type) (decoder, error) { +func makeStructDecoder(typ reflect.Type, flat bool) (decoder, error) { fields, err := structFields(typ) if err != nil { return nil, err @@ -387,8 +387,10 @@ func makeStructDecoder(typ reflect.Type) (decoder, error) { } } dec := func(s *Stream, val reflect.Value) (err error) { - if _, err := s.List(); err != nil { - return wrapStreamError(err, typ) + if !flat { + if _, err := s.List(); err != nil { + return wrapStreamError(err, typ) + } } for i, f := range fields { err := f.info.decoder(s, val.Field(f.index)) @@ -405,6 +407,9 @@ func makeStructDecoder(typ reflect.Type) (decoder, error) { return addErrorContext(err, "."+typ.Field(f.index).Name) } } + if flat { + return nil + } return wrapStreamError(s.ListEnd(), typ) } return dec, nil diff --git a/rlp/encode.go b/rlp/encode.go index b96505f56d..639c2e56e7 100644 --- a/rlp/encode.go +++ b/rlp/encode.go @@ -158,7 +158,7 @@ func makeWriter(typ reflect.Type, ts rlpstruct.Tags) (writer, error) { case kind == reflect.Slice || kind == reflect.Array: return makeSliceWriter(typ, ts) case kind == reflect.Struct: - return makeStructWriter(typ) + return makeStructWriter(typ, ts.Flat) case kind == reflect.Interface: return writeInterface, nil default: @@ -315,7 +315,7 @@ func makeSliceWriter(typ reflect.Type, ts rlpstruct.Tags) (writer, error) { return wfn, nil } -func makeStructWriter(typ reflect.Type) (writer, error) { +func makeStructWriter(typ reflect.Type, flat bool) (writer, error) { fields, err := structFields(typ) if err != nil { return nil, err @@ -331,13 +331,18 @@ func makeStructWriter(typ reflect.Type) (writer, error) { if firstOptionalField == len(fields) { // This is the writer function for structs without any optional fields. writer = func(val reflect.Value, w *encBuffer) error { - lh := w.list() + var lh int + if !flat { + lh = w.list() + } for _, f := range fields { if err := f.info.writer(val.Field(f.index), w); err != nil { return err } } - w.listEnd(lh) + if !flat { + w.listEnd(lh) + } return nil } } else { @@ -350,13 +355,18 @@ func makeStructWriter(typ reflect.Type) (writer, error) { break } } - lh := w.list() + var lh int + if !flat { + lh = w.list() + } for i := 0; i <= lastField; i++ { if err := fields[i].info.writer(val.Field(fields[i].index), w); err != nil { return err } } - w.listEnd(lh) + if !flat { + w.listEnd(lh) + } return nil } } diff --git a/rlp/internal/rlpstruct/rlpstruct.go b/rlp/internal/rlpstruct/rlpstruct.go index 1edead96ce..1456cc3c07 100644 --- a/rlp/internal/rlpstruct/rlpstruct.go +++ b/rlp/internal/rlpstruct/rlpstruct.go @@ -79,6 +79,9 @@ type Tags struct { // rlp:"-" ignores fields. Ignored bool + + // rlp:"flat" flattens field. + Flat bool } // TagError is raised for invalid struct tags. @@ -183,6 +186,8 @@ func parseTag(field Field, lastPublic int) (Tags, error) { if field.Type.Kind != reflect.Slice { return ts, TagError{Field: name, Tag: t, Err: "field type is not slice"} } + case "flat": + ts.Flat = true default: return ts, TagError{Field: name, Tag: t, Err: "unknown tag"} }