diff --git a/app/submodule/eth/eth_api.go b/app/submodule/eth/eth_api.go index 8fdc36ce83..782734bf51 100644 --- a/app/submodule/eth/eth_api.go +++ b/app/submodule/eth/eth_api.go @@ -269,10 +269,23 @@ func (a *ethAPI) EthGetTransactionByHashLimited(ctx context.Context, txHash *typ for _, p := range pending { if p.Cid() == c { - tx, err := newEthTxFromSignedMessage(ctx, p, a.chain) + // We only return pending eth-account messages because we can't guarantee + // that the from/to addresses of other messages are conversable to 0x-style + // addresses. So we just ignore them. + // + // This should be "fine" as anyone using an "Ethereum-centric" block + // explorer shouldn't care about seeing pending messages from native + // accounts. + ethtx, err := types.EthTransactionFromSignedFilecoinMessage(p) if err != nil { - return nil, fmt.Errorf("could not convert Filecoin message into tx: %v", err) + return nil, fmt.Errorf("could not convert Filecoin message into tx: %w", err) } + + tx, err := ethtx.ToEthTx(p) + if err != nil { + return nil, fmt.Errorf("could not convert Eth transaction to EthTx: %w", err) + } + return &tx, nil } } @@ -318,7 +331,7 @@ func (a *ethAPI) EthGetMessageCidByTransactionHash(ctx context.Context, txHash * } func (a *ethAPI) EthGetTransactionHashByCid(ctx context.Context, cid cid.Cid) (*types.EthHash, error) { - hash, err := ethTxHashFromMessageCid(ctx, cid, a.em.chainModule.MessageStore, a.chain) + hash, err := ethTxHashFromMessageCid(ctx, cid, a.em.chainModule.MessageStore) if hash == types.EmptyEthHash { // not found return nil, nil @@ -800,12 +813,12 @@ func (a *ethAPI) EthGasPrice(ctx context.Context) (types.EthBigInt, error) { } func (a *ethAPI) EthSendRawTransaction(ctx context.Context, rawTx types.EthBytes) (types.EthHash, error) { - txArgs, err := types.ParseEthTxArgs(rawTx) + txArgs, err := types.ParseEthTransaction(rawTx) if err != nil { return types.EmptyEthHash, err } - smsg, err := txArgs.ToSignedMessage() + smsg, err := types.ToSignedFilecoinMessage(txArgs) if err != nil { return types.EmptyEthHash, err } diff --git a/app/submodule/eth/eth_event_api.go b/app/submodule/eth/eth_event_api.go index 59ad16b888..2984d813bd 100644 --- a/app/submodule/eth/eth_event_api.go +++ b/app/submodule/eth/eth_event_api.go @@ -178,7 +178,7 @@ func (e *ethEventAPI) EthGetFilterChanges(ctx context.Context, id types.EthFilte case filterTipSetCollector: return ethFilterResultFromTipSets(fc.TakeCollectedTipSets(ctx)) case filterMessageCollector: - return ethFilterResultFromMessages(fc.TakeCollectedMessages(ctx), e.ChainAPI) + return ethFilterResultFromMessages(fc.TakeCollectedMessages(ctx)) } return nil, fmt.Errorf("unknown filter type") @@ -649,7 +649,7 @@ func ethFilterResultFromEvents(evs []*filter.CollectedEvent, ms *chain.MessageSt return nil, err } - log.TransactionHash, err = ethTxHashFromMessageCid(context.TODO(), ev.MsgCid, ms, ca) + log.TransactionHash, err = ethTxHashFromMessageCid(context.TODO(), ev.MsgCid, ms) if err != nil { return nil, err } @@ -692,11 +692,11 @@ func ethFilterResultFromTipSets(tsks []types.TipSetKey) (*types.EthFilterResult, return res, nil } -func ethFilterResultFromMessages(cs []*types.SignedMessage, ca v1.IChain) (*types.EthFilterResult, error) { +func ethFilterResultFromMessages(cs []*types.SignedMessage) (*types.EthFilterResult, error) { res := &types.EthFilterResult{} for _, c := range cs { - hash, err := ethTxHashFromSignedMessage(context.TODO(), c, ca) + hash, err := ethTxHashFromSignedMessage(c) if err != nil { return nil, err } @@ -896,7 +896,7 @@ func (e *ethSubscription) start(ctx context.Context) { e.send(ctx, ethBlock) e.lastSentTipset = &parentTipSetKey case *types.SignedMessage: // mpool txid - evs, err := ethFilterResultFromMessages([]*types.SignedMessage{vt}, e.chainAPI) + evs, err := ethFilterResultFromMessages([]*types.SignedMessage{vt}) if err != nil { continue } diff --git a/app/submodule/eth/eth_utils.go b/app/submodule/eth/eth_utils.go index bc1bdc9815..00bcfd701e 100644 --- a/app/submodule/eth/eth_utils.go +++ b/app/submodule/eth/eth_utils.go @@ -8,11 +8,13 @@ import ( "fmt" "github.com/ipfs/go-cid" + "github.com/multiformats/go-multicodec" "github.com/filecoin-project/go-address" "github.com/filecoin-project/go-state-types/abi" "github.com/filecoin-project/go-state-types/big" "github.com/filecoin-project/go-state-types/builtin" + builtintypes "github.com/filecoin-project/go-state-types/builtin" "github.com/filecoin-project/go-state-types/builtin/v10/eam" "github.com/filecoin-project/go-state-types/crypto" "github.com/filecoin-project/go-state-types/exitcode" @@ -26,6 +28,18 @@ import ( "github.com/filecoin-project/venus/venus-shared/types" ) +// The address used in messages to actors that have since been deleted. +// +// 0xff0000000000000000000000ffffffffffffffff +var revertedEthAddress types.EthAddress + +func init() { + revertedEthAddress[0] = 0xff + for i := 20 - 8; i < 20; i++ { + revertedEthAddress[i] = 0xff + } +} + func getTipsetByBlockNumber(ctx context.Context, store *chain.Store, blkParam string, strict bool) (*types.TipSet, error) { if blkParam == "earliest" { return nil, fmt.Errorf("block param \"earliest\" is not supported") @@ -231,7 +245,6 @@ func newEthBlockFromFilecoinTipSet(ctx context.Context, ts *types.TipSet, fullTx return types.EthBlock{}, fmt.Errorf("failed to convert msg to ethTx: %w", err) } - tx.ChainID = types.EthUint64(types2.Eip155ChainID) tx.BlockHash = &blkHash tx.BlockNumber = &bn tx.TransactionIndex = &ti @@ -377,8 +390,10 @@ func lookupEthAddress(ctx context.Context, addr address.Address, ca v1.IChain) ( return types.EthAddress{}, err } + // revive:disable:empty-block easier to grok when the cases are explicit + // Lookup on the target actor and try to get an f410 address. - if actor, err := ca.GetActor(ctx, idAddr); errors.Is(err, types.ErrActorNotFound) { + if actor, err := ca.StateGetActor(ctx, idAddr, types.EmptyTSK); errors.Is(err, types.ErrActorNotFound) { // Not found -> use a masked ID address } else if err != nil { // Any other error -> fail. @@ -394,28 +409,35 @@ func lookupEthAddress(ctx context.Context, addr address.Address, ca v1.IChain) ( return types.EthAddressFromFilecoinAddress(idAddr) } -func ethTxHashFromMessageCid(ctx context.Context, c cid.Cid, ms *chain.MessageStore, ca v1.IChain) (types.EthHash, error) { +func ethTxHashFromMessageCid(ctx context.Context, c cid.Cid, ms *chain.MessageStore) (types.EthHash, error) { smsg, err := ms.LoadSignedMessage(ctx, c) if err == nil { // This is an Eth Tx, Secp message, Or BLS message in the mpool - return ethTxHashFromSignedMessage(ctx, smsg, ca) + return ethTxHashFromSignedMessage(smsg) + } + + _, err = ms.LoadMessage(ctx, c) + if err == nil { + // This is a BLS message + return types.EthHashFromCid(c) } return types.EthHashFromCid(c) } -func ethTxHashFromSignedMessage(ctx context.Context, smsg *types.SignedMessage, ca v1.IChain) (types.EthHash, error) { +func ethTxHashFromSignedMessage(smsg *types.SignedMessage) (types.EthHash, error) { if smsg.Signature.Type == crypto.SigTypeDelegated { - ethTx, err := newEthTxFromSignedMessage(ctx, smsg, ca) + tx, err := types.EthTransactionFromSignedFilecoinMessage(smsg) if err != nil { - return types.EmptyEthHash, err + return types.EthHash{}, fmt.Errorf("failed to convert from signed message: %w", err) } - return ethTx.Hash, nil + + return tx.TxHash() } else if smsg.Signature.Type == crypto.SigTypeSecp256k1 { return types.EthHashFromCid(smsg.Cid()) - } else { // BLS message - return types.EthHashFromCid(smsg.Message.Cid()) } + // else BLS message + return types.EthHashFromCid(smsg.Message.Cid()) } func newEthTxFromSignedMessage(ctx context.Context, smsg *types.SignedMessage, ca v1.IChain) (types.EthTx, error) { @@ -424,33 +446,31 @@ func newEthTxFromSignedMessage(ctx context.Context, smsg *types.SignedMessage, c // This is an eth tx if smsg.Signature.Type == crypto.SigTypeDelegated { - tx, err = types.EthTxFromSignedEthMessage(smsg) + ethTx, err := types.EthTransactionFromSignedFilecoinMessage(smsg) if err != nil { return types.EthTx{}, fmt.Errorf("failed to convert from signed message: %w", err) } - - tx.Hash, err = tx.TxHash() + tx, err = ethTx.ToEthTx(smsg) if err != nil { - return types.EthTx{}, fmt.Errorf("failed to calculate hash for ethTx: %w", err) + return types.EthTx{}, fmt.Errorf("failed to convert from signed message: %w", err) } - - fromAddr, err := lookupEthAddress(ctx, smsg.Message.From, ca) + } else if smsg.Signature.Type == crypto.SigTypeSecp256k1 { // Secp Filecoin Message + tx, err = ethTxFromNativeMessage(ctx, smsg.VMMessage(), ca) if err != nil { - return types.EthTx{}, fmt.Errorf("failed to resolve Ethereum address: %w", err) + return types.EthTx{}, err } - - tx.From = fromAddr - } else if smsg.Signature.Type == crypto.SigTypeSecp256k1 { // Secp Filecoin Message - tx = ethTxFromNativeMessage(ctx, smsg.VMMessage(), ca) tx.Hash, err = types.EthHashFromCid(smsg.Cid()) if err != nil { - return tx, err + return types.EthTx{}, err } } else { // BLS Filecoin message - tx = ethTxFromNativeMessage(ctx, smsg.VMMessage(), ca) + tx, err = ethTxFromNativeMessage(ctx, smsg.VMMessage(), ca) + if err != nil { + return types.EthTx{}, err + } tx.Hash, err = types.EthHashFromCid(smsg.Message.Cid()) if err != nil { - return tx, err + return types.EthTx{}, err } } @@ -473,27 +493,76 @@ func parseEthTopics(topics types.EthTopicSpec) (map[string][][]byte, error) { return keys, nil } +// Convert a native message to an eth transaction. +// +// - The state-tree must be from after the message was applied (ideally the following tipset). +// - In some cases, the "to" address may be `0xff0000000000000000000000ffffffffffffffff`. This +// means that the "to" address has not been assigned in the passed state-tree and can only +// happen if the transaction reverted. +// // ethTxFromNativeMessage does NOT populate: // - BlockHash // - BlockNumber // - TransactionIndex // - Hash -func ethTxFromNativeMessage(ctx context.Context, msg *types.Message, ca v1.IChain) types.EthTx { - // We don't care if we error here, conversion is best effort for non-eth transactions - from, _ := lookupEthAddress(ctx, msg.From, ca) - to, _ := lookupEthAddress(ctx, msg.To, ca) - return types.EthTx{ +func ethTxFromNativeMessage(ctx context.Context, msg *types.Message, ca v1.IChain) (types.EthTx, error) { + // Lookup the from address. This must succeed. + from, err := lookupEthAddress(ctx, msg.From, ca) + if err != nil { + return types.EthTx{}, fmt.Errorf("failed to lookup sender address %s when converting a native message to an eth txn: %w", msg.From, err) + } + // Lookup the to address. If the recipient doesn't exist, we replace the address with a + // known sentinel address. + to, err := lookupEthAddress(ctx, msg.To, ca) + if err != nil { + if !errors.Is(err, types.ErrActorNotFound) { + return types.EthTx{}, fmt.Errorf("failed to lookup receiver address %s when converting a native message to an eth txn: %w", msg.To, err) + } + to = revertedEthAddress + } + + // For empty, we use "0" as the codec. Otherwise, we use CBOR for message + // parameters. + var codec uint64 + if len(msg.Params) > 0 { + codec = uint64(multicodec.Cbor) + } + + maxFeePerGas := types.EthBigInt(msg.GasFeeCap) + maxPriorityFeePerGas := types.EthBigInt(msg.GasPremium) + + // We decode as a native call first. + ethTx := types.EthTx{ To: &to, From: from, + Input: encodeFilecoinParamsAsABI(msg.Method, codec, msg.Params), Nonce: types.EthUint64(msg.Nonce), ChainID: types.EthUint64(types2.Eip155ChainID), Value: types.EthBigInt(msg.Value), - Type: types.Eip1559TxType, + Type: types.EIP1559TxType, Gas: types.EthUint64(msg.GasLimit), - MaxFeePerGas: types.EthBigInt(msg.GasFeeCap), - MaxPriorityFeePerGas: types.EthBigInt(msg.GasPremium), + MaxFeePerGas: &maxFeePerGas, + MaxPriorityFeePerGas: &maxPriorityFeePerGas, AccessList: []types.EthHash{}, } + + // Then we try to see if it's "special". If we fail, we ignore the error and keep treating + // it as a native message. Unfortunately, the user is free to send garbage that may not + // properly decode. + if msg.Method == builtintypes.MethodsEVM.InvokeContract { + // try to decode it as a contract invocation first. + if inp, err := decodePayload(msg.Params, codec); err == nil { + ethTx.Input = []byte(inp) + } + } else if msg.To == builtin.EthereumAddressManagerActorAddr && msg.Method == builtintypes.MethodsEAM.CreateExternal { + // Then, try to decode it as a contract deployment from an EOA. + if inp, err := decodePayload(msg.Params, codec); err == nil { + ethTx.Input = []byte(inp) + ethTx.To = nil + } + } + + return ethTx, nil } // newEthTxFromMessageLookup creates an ethereum transaction from filecoin message lookup. If a negative txIdx is passed @@ -618,7 +687,18 @@ func newEthTxReceipt(ctx context.Context, tx types.EthTx, lookup *types.MsgLooku } baseFee := parentTS.Blocks()[0].ParentBaseFee - gasOutputs := gas.ComputeGasOutputs(lookup.Receipt.GasUsed, int64(tx.Gas), baseFee, big.Int(tx.MaxFeePerGas), big.Int(tx.MaxPriorityFeePerGas), true) + + gasFeeCap, err := tx.GasFeeCap() + if err != nil { + return types.EthTxReceipt{}, fmt.Errorf("failed to get gas fee cap: %w", err) + } + gasPremium, err := tx.GasPremium() + if err != nil { + return types.EthTxReceipt{}, fmt.Errorf("failed to get gas premium: %w", err) + } + + gasOutputs := gas.ComputeGasOutputs(lookup.Receipt.GasUsed, int64(tx.Gas), baseFee, big.Int(gasFeeCap), + big.Int(gasPremium), true) totalSpent := big.Sum(gasOutputs.BaseFeeBurn, gasOutputs.MinerTip, gasOutputs.OverEstimationBurn) effectiveGasPrice := big.Zero() diff --git a/app/submodule/eth/txhashmanager.go b/app/submodule/eth/txhashmanager.go index b45b7c0297..235d419626 100644 --- a/app/submodule/eth/txhashmanager.go +++ b/app/submodule/eth/txhashmanager.go @@ -32,7 +32,7 @@ func (m *ethTxHashManager) Apply(ctx context.Context, from, to *types.TipSet) er continue } - hash, err := ethTxHashFromSignedMessage(ctx, smsg, m.chainAPI) + hash, err := ethTxHashFromSignedMessage(smsg) if err != nil { return err } @@ -89,13 +89,18 @@ func (m *ethTxHashManager) ProcessSignedMessage(ctx context.Context, msg *types. return } - ethTx, err := newEthTxFromSignedMessage(ctx, msg, m.chainAPI) + ethTx, err := types.EthTransactionFromSignedFilecoinMessage(msg) if err != nil { log.Errorf("error converting filecoin message to eth tx: %s", err) return } - err = m.TransactionHashLookup.UpsertHash(ethTx.Hash, msg.Cid()) + txHash, err := ethTx.TxHash() + if err != nil { + log.Errorf("error hashing transaction: %s", err) + return + } + err = m.TransactionHashLookup.UpsertHash(txHash, msg.Cid()) if err != nil { log.Errorf("error inserting tx mapping to db: %s", err) return diff --git a/pkg/chain/signatures.go b/pkg/chain/signatures.go index b2517cc452..d06afc24cc 100644 --- a/pkg/chain/signatures.go +++ b/pkg/chain/signatures.go @@ -17,33 +17,42 @@ import ( func AuthenticateMessage(msg *types.SignedMessage, signer address.Address) error { var digest []byte - typ := msg.Signature.Type - switch typ { + signatureType := msg.Signature.Type + signatureCopy := msg.Signature + + switch signatureType { case crypto.SigTypeDelegated: - txArgs, err := types.EthTxArgsFromUnsignedEthMessage(&msg.Message) + signatureCopy.Data = make([]byte, len(msg.Signature.Data)) + copy(signatureCopy.Data, msg.Signature.Data) + ethTx, err := types.EthTransactionFromSignedFilecoinMessage(msg) if err != nil { - return fmt.Errorf("failed to reconstruct eth transaction: %w", err) + return fmt.Errorf("failed to reconstruct Ethereum transaction: %w", err) } - roundTripMsg, err := txArgs.ToUnsignedMessage(msg.Message.From) + + filecoinMsg, err := ethTx.ToUnsignedFilecoinMessage(msg.Message.From) if err != nil { - return fmt.Errorf("failed to reconstruct filecoin msg: %w", err) + return fmt.Errorf("failed to reconstruct Filecoin message: %w", err) } - if !msg.Message.Equals(roundTripMsg) { - return fmt.Errorf("ethereum tx failed to roundtrip") + if !msg.Message.Equals(filecoinMsg) { + return fmt.Errorf("ethereum transaction roundtrip mismatch") } - rlpEncodedMsg, err := txArgs.ToRlpUnsignedMsg() + rlpEncodedMsg, err := ethTx.ToRlpUnsignedMsg() if err != nil { - return fmt.Errorf("failed to repack eth rlp message: %w", err) + return fmt.Errorf("failed to encode RLP message: %w", err) } digest = rlpEncodedMsg + signatureCopy.Data, err = ethTx.ToVerifiableSignature(signatureCopy.Data) + if err != nil { + return fmt.Errorf("failed to verify signature: %w", err) + } default: digest = msg.Message.Cid().Bytes() } - if err := crypto.Verify(&msg.Signature, signer, digest); err != nil { - return fmt.Errorf("message %s has invalid signature (type %d): %w", msg.Cid(), typ, err) + if err := crypto.Verify(&signatureCopy, signer, digest); err != nil { + return fmt.Errorf("invalid signature for message %s (type %d): %w", msg.Cid(), signatureType, err) } return nil } diff --git a/pkg/consensus/block_validator.go b/pkg/consensus/block_validator.go index ccc33d29b4..33dc49a98d 100644 --- a/pkg/consensus/block_validator.go +++ b/pkg/consensus/block_validator.go @@ -747,6 +747,18 @@ func (bv *BlockValidator) VerifyWinningPoStProof(ctx context.Context, nv network return nil } +func IsValidEthTxForSending(nv network.Version, smsg *types.SignedMessage) bool { + ethTx, err := types.EthTransactionFromSignedFilecoinMessage(smsg) + if err != nil { + return false + } + + if nv < network.Version23 && ethTx.Type() != types.EIP1559TxType { + return false + } + return true +} + func IsValidForSending(nv network.Version, act *types.Actor) bool { // Before nv18 (Hygge), we only supported built-in account actors as senders. // @@ -893,6 +905,10 @@ func (bv *BlockValidator) checkBlockMessages(ctx context.Context, return fmt.Errorf("block had invalid secpk message at index %d: %v", i, err) } + if m.Signature.Type == crypto.SigTypeDelegated && !IsValidEthTxForSending(nv, m) { + return fmt.Errorf("network version should be atleast NV23 for sending legacy ETH transactions; but current network version is %d", nv) + } + secpMsgs[i] = m } diff --git a/pkg/messagepool/messagepool.go b/pkg/messagepool/messagepool.go index f9ea785568..49754e38cf 100644 --- a/pkg/messagepool/messagepool.go +++ b/pkg/messagepool/messagepool.go @@ -958,6 +958,11 @@ func (mp *MessagePool) addTS(ctx context.Context, m *types.SignedMessage, curTS nv := mp.api.StateNetworkVersion(ctx, epoch) // TODO: I'm not thrilled about depending on filcns here, but I prefer this to duplicating logic + + if m.Signature.Type == crypto.SigTypeDelegated && !consensus.IsValidEthTxForSending(nv, m) { + return false, fmt.Errorf("network version should be atleast NV23 for sending legacy ETH transactions; but current network version is %d", nv) + } + if !consensus.IsValidForSending(nv, senderAct) { return false, fmt.Errorf("sender actor %s is not a valid top-level sender", m.Message.From) } diff --git a/venus-shared/actors/types/eth.go b/venus-shared/actors/types/eth.go index abb73537c6..fabe931928 100644 --- a/venus-shared/actors/types/eth.go +++ b/venus-shared/actors/types/eth.go @@ -30,6 +30,7 @@ var Eip155ChainID = 314 func SetEip155ChainID(val int) { Eip155ChainID = val + initEthLegacy155TxSignature() } type EthUint64 uint64 diff --git a/venus-shared/actors/types/eth_1559_transactions.go b/venus-shared/actors/types/eth_1559_transactions.go new file mode 100644 index 0000000000..4a41e92d48 --- /dev/null +++ b/venus-shared/actors/types/eth_1559_transactions.go @@ -0,0 +1,339 @@ +package types + +import ( + "fmt" + + "golang.org/x/xerrors" + + "github.com/filecoin-project/go-address" + "github.com/filecoin-project/go-state-types/big" + typescrypto "github.com/filecoin-project/go-state-types/crypto" +) + +var _ EthTransaction = (*Eth1559TxArgs)(nil) + +type Eth1559TxArgs struct { + ChainID int `json:"chainId"` + Nonce int `json:"nonce"` + To *EthAddress `json:"to"` + Value big.Int `json:"value"` + MaxFeePerGas big.Int `json:"maxFeePerGas"` + MaxPriorityFeePerGas big.Int `json:"maxPriorityFeePerGas"` + GasLimit int `json:"gasLimit"` + Input []byte `json:"input"` + V big.Int `json:"v"` + R big.Int `json:"r"` + S big.Int `json:"s"` +} + +func (tx *Eth1559TxArgs) ToUnsignedFilecoinMessage(from address.Address) (*Message, error) { + if tx.ChainID != Eip155ChainID { + return nil, fmt.Errorf("invalid chain id: %d", tx.ChainID) + } + mi, err := getFilecoinMethodInfo(tx.To, tx.Input) + if err != nil { + return nil, xerrors.Errorf("failed to get method info: %w", err) + } + + return &Message{ + Version: 0, + To: mi.to, + From: from, + Nonce: uint64(tx.Nonce), + Value: tx.Value, + GasLimit: int64(tx.GasLimit), + GasFeeCap: tx.MaxFeePerGas, + GasPremium: tx.MaxPriorityFeePerGas, + Method: mi.method, + Params: mi.params, + }, nil +} + +func (tx *Eth1559TxArgs) ToRlpUnsignedMsg() ([]byte, error) { + encoded, err := toRlpUnsignedMsg(tx) + if err != nil { + return nil, err + } + return append([]byte{EIP1559TxType}, encoded...), nil +} + +func (tx *Eth1559TxArgs) TxHash() (EthHash, error) { + rlp, err := tx.ToRlpSignedMsg() + if err != nil { + return EmptyEthHash, err + } + + return EthHashFromTxBytes(rlp), nil +} + +func (tx *Eth1559TxArgs) ToRlpSignedMsg() ([]byte, error) { + encoded, err := toRlpSignedMsg(tx, tx.V, tx.R, tx.S) + if err != nil { + return nil, err + } + return append([]byte{EIP1559TxType}, encoded...), nil +} + +func (tx *Eth1559TxArgs) Signature() (*typescrypto.Signature, error) { + r := tx.R.Int.Bytes() + s := tx.S.Int.Bytes() + v := tx.V.Int.Bytes() + + sig := append([]byte{}, padLeadingZeros(r, 32)...) + sig = append(sig, padLeadingZeros(s, 32)...) + if len(v) == 0 { + sig = append(sig, 0) + } else { + sig = append(sig, v[0]) + } + + if len(sig) != 65 { + return nil, xerrors.Errorf("signature is not 65 bytes") + } + return &typescrypto.Signature{ + Type: typescrypto.SigTypeDelegated, Data: sig, + }, nil +} + +func (tx *Eth1559TxArgs) Sender() (address.Address, error) { + return sender(tx) +} + +func (tx *Eth1559TxArgs) Type() int { + return EIP1559TxType +} + +func (tx *Eth1559TxArgs) ToVerifiableSignature(sig []byte) ([]byte, error) { + return sig, nil +} + +func (tx *Eth1559TxArgs) ToEthTx(smsg *SignedMessage) (EthTx, error) { + from, err := EthAddressFromFilecoinAddress(smsg.Message.From) + if err != nil { + return EthTx{}, xerrors.Errorf("sender was not an eth account") + } + hash, err := tx.TxHash() + if err != nil { + return EthTx{}, err + } + gasFeeCap := EthBigInt(tx.MaxFeePerGas) + gasPremium := EthBigInt(tx.MaxPriorityFeePerGas) + + ethTx := EthTx{ + ChainID: EthUint64(Eip155ChainID), + Type: EIP1559TxType, + Nonce: EthUint64(tx.Nonce), + Hash: hash, + To: tx.To, + Value: EthBigInt(tx.Value), + Input: tx.Input, + Gas: EthUint64(tx.GasLimit), + MaxFeePerGas: &gasFeeCap, + MaxPriorityFeePerGas: &gasPremium, + From: from, + R: EthBigInt(tx.R), + S: EthBigInt(tx.S), + V: EthBigInt(tx.V), + } + + return ethTx, nil +} + +func (tx *Eth1559TxArgs) InitialiseSignature(sig typescrypto.Signature) error { + if sig.Type != typescrypto.SigTypeDelegated { + return xerrors.Errorf("RecoverSignature only supports Delegated signature") + } + + if len(sig.Data) != EthEIP1559TxSignatureLen { + return xerrors.Errorf("signature should be 65 bytes long, but got %d bytes", len(sig.Data)) + } + + r_, err := parseBigInt(sig.Data[0:32]) + if err != nil { + return xerrors.Errorf("cannot parse r into EthBigInt") + } + + s_, err := parseBigInt(sig.Data[32:64]) + if err != nil { + return xerrors.Errorf("cannot parse s into EthBigInt") + } + + v_, err := parseBigInt([]byte{sig.Data[64]}) + if err != nil { + return xerrors.Errorf("cannot parse v into EthBigInt") + } + + tx.R = r_ + tx.S = s_ + tx.V = v_ + + return nil +} + +func (tx *Eth1559TxArgs) packTxFields() ([]interface{}, error) { + chainId, err := formatInt(tx.ChainID) + if err != nil { + return nil, err + } + + nonce, err := formatInt(tx.Nonce) + if err != nil { + return nil, err + } + + maxPriorityFeePerGas, err := formatBigInt(tx.MaxPriorityFeePerGas) + if err != nil { + return nil, err + } + + maxFeePerGas, err := formatBigInt(tx.MaxFeePerGas) + if err != nil { + return nil, err + } + + gasLimit, err := formatInt(tx.GasLimit) + if err != nil { + return nil, err + } + + value, err := formatBigInt(tx.Value) + if err != nil { + return nil, err + } + + res := []interface{}{ + chainId, + nonce, + maxPriorityFeePerGas, + maxFeePerGas, + gasLimit, + formatEthAddr(tx.To), + value, + tx.Input, + []interface{}{}, // access list + } + return res, nil +} + +func parseEip1559Tx(data []byte) (*Eth1559TxArgs, error) { + if data[0] != EIP1559TxType { + return nil, xerrors.Errorf("not an EIP-1559 transaction: first byte is not %d", EIP1559TxType) + } + + d, err := DecodeRLP(data[1:]) + if err != nil { + return nil, err + } + decoded, ok := d.([]interface{}) + if !ok { + return nil, xerrors.Errorf("not an EIP-1559 transaction: decoded data is not a list") + } + + if len(decoded) != 12 { + return nil, xerrors.Errorf("not an EIP-1559 transaction: should have 12 elements in the rlp list") + } + + chainId, err := parseInt(decoded[0]) + if err != nil { + return nil, err + } + + nonce, err := parseInt(decoded[1]) + if err != nil { + return nil, err + } + + maxPriorityFeePerGas, err := parseBigInt(decoded[2]) + if err != nil { + return nil, err + } + + maxFeePerGas, err := parseBigInt(decoded[3]) + if err != nil { + return nil, err + } + + gasLimit, err := parseInt(decoded[4]) + if err != nil { + return nil, err + } + + to, err := parseEthAddr(decoded[5]) + if err != nil { + return nil, err + } + + value, err := parseBigInt(decoded[6]) + if err != nil { + return nil, err + } + + input, err := parseBytes(decoded[7]) + if err != nil { + return nil, err + } + + accessList, ok := decoded[8].([]interface{}) + if !ok || (ok && len(accessList) != 0) { + return nil, xerrors.Errorf("access list should be an empty list") + } + + r, err := parseBigInt(decoded[10]) + if err != nil { + return nil, err + } + + s, err := parseBigInt(decoded[11]) + if err != nil { + return nil, err + } + + v, err := parseBigInt(decoded[9]) + if err != nil { + return nil, err + } + + // EIP-1559 and EIP-2930 transactions only support 0 or 1 for v + // Legacy and EIP-155 transactions support other values + // https://github.com/ethers-io/ethers.js/blob/56fabe987bb8c1e4891fdf1e5d3fe8a4c0471751/packages/transactions/src.ts/index.ts#L333 + if !v.Equals(big.NewInt(0)) && !v.Equals(big.NewInt(1)) { + return nil, xerrors.Errorf("EIP-1559 transactions only support 0 or 1 for v") + } + + args := Eth1559TxArgs{ + ChainID: chainId, + Nonce: nonce, + To: to, + MaxPriorityFeePerGas: maxPriorityFeePerGas, + MaxFeePerGas: maxFeePerGas, + GasLimit: gasLimit, + Value: value, + Input: input, + V: v, + R: r, + S: s, + } + return &args, nil +} + +func Eth1559TxArgsFromUnsignedFilecoinMessage(msg *Message) (*Eth1559TxArgs, error) { + if msg.Version != 0 { + return nil, fmt.Errorf("unsupported msg version: %d", msg.Version) + } + + params, to, err := getEthParamsAndRecipient(msg) + if err != nil { + return nil, fmt.Errorf("failed to get eth params and recipient: %w", err) + } + + return &Eth1559TxArgs{ + ChainID: Eip155ChainID, + Nonce: int(msg.Nonce), + To: to, + Value: msg.Value, + Input: params, + MaxFeePerGas: msg.GasFeeCap, + MaxPriorityFeePerGas: msg.GasPremium, + GasLimit: int(msg.GasLimit), + }, nil +} diff --git a/venus-shared/types/eth_transactions_test.go b/venus-shared/actors/types/eth_1559_transactions_test.go similarity index 98% rename from venus-shared/types/eth_transactions_test.go rename to venus-shared/actors/types/eth_1559_transactions_test.go index 82cf8c6412..92ebd48a0b 100644 --- a/venus-shared/types/eth_transactions_test.go +++ b/venus-shared/actors/types/eth_1559_transactions_test.go @@ -27,10 +27,10 @@ type TxTestcase struct { TxJSON string NosigTx string Input EthBytes - Output EthTxArgs + Output Eth1559TxArgs } -func TestTxArgs(t *testing.T) { +func TestEIP1559TxArgs(t *testing.T) { testcases, err := prepareTxTestcases() require.Nil(t, err) require.NotEmpty(t, testcases) @@ -39,31 +39,31 @@ func TestTxArgs(t *testing.T) { comment := fmt.Sprintf("case %d: \n%s\n%s", i, tc.TxJSON, hex.EncodeToString(tc.Input)) // parse txargs - txArgs, err := ParseEthTxArgs(tc.Input) - require.NoErrorf(t, err, comment) + txArgs, err := parseEip1559Tx(tc.Input) + require.NoError(t, err, comment) msgRecovered, err := txArgs.ToRlpUnsignedMsg() - require.NoErrorf(t, err, comment) + require.NoError(t, err, comment) require.Equal(t, tc.NosigTx, "0x"+hex.EncodeToString(msgRecovered), comment) // verify signatures from, err := txArgs.Sender() - require.NoErrorf(t, err, comment) + require.NoError(t, err, comment) - smsg, err := txArgs.ToSignedMessage() - require.NoErrorf(t, err, comment) + smsg, err := ToSignedFilecoinMessage(txArgs) + require.NoError(t, err, comment) err = crypto.Verify(&smsg.Signature, from, msgRecovered) - require.NoErrorf(t, err, comment) + require.NoError(t, err, comment) // verify data - require.Equal(t, tc.Output.ChainID, txArgs.ChainID) - require.Equal(t, tc.Output.Nonce, txArgs.Nonce) - require.Equal(t, tc.Output.To, txArgs.To) + require.Equal(t, tc.Output.ChainID, txArgs.ChainID, comment) + require.Equal(t, tc.Output.Nonce, txArgs.Nonce, comment) + require.Equal(t, tc.Output.To, txArgs.To, comment) } } -func TestSignatures(t *testing.T) { +func TestEIP1559Signatures(t *testing.T) { testcases := []struct { RawTx string ExpectedR string @@ -73,29 +73,29 @@ func TestSignatures(t *testing.T) { }{ { "0x02f8598401df5e76028301d69083086a5e835532dd808080c080a0457e33227ac7ceee2ef121755e26b872b6fb04221993f9939349bb7b0a3e1595a02d8ef379e1d2a9e30fa61c92623cc9ed72d80cf6a48cfea341cb916bcc0a81bc", - `"0x457e33227ac7ceee2ef121755e26b872b6fb04221993f9939349bb7b0a3e1595"`, - `"0x2d8ef379e1d2a9e30fa61c92623cc9ed72d80cf6a48cfea341cb916bcc0a81bc"`, - `"0x0"`, + "0x457e33227ac7ceee2ef121755e26b872b6fb04221993f9939349bb7b0a3e1595", + "0x2d8ef379e1d2a9e30fa61c92623cc9ed72d80cf6a48cfea341cb916bcc0a81bc", + "0x0", false, }, { "0x02f8598401df5e76038301d69083086a5e835532dd808080c001a012a232866dcb0671eb0ddc01fb9c01d6ef384ec892bb29691ed0d2d293052ddfa052a6ae38c6139930db21a00eee2a4caced9a6500991b823d64ec664d003bc4b1", - `"0x12a232866dcb0671eb0ddc01fb9c01d6ef384ec892bb29691ed0d2d293052ddf"`, - `"0x52a6ae38c6139930db21a00eee2a4caced9a6500991b823d64ec664d003bc4b1"`, - `"0x1"`, + "0x12a232866dcb0671eb0ddc01fb9c01d6ef384ec892bb29691ed0d2d293052ddf", + "0x52a6ae38c6139930db21a00eee2a4caced9a6500991b823d64ec664d003bc4b1", + "0x1", false, }, { "0x00", - `""`, - `""`, - `""`, + "", + "", + "", true, }, } for _, tc := range testcases { - tx, err := ParseEthTxArgs(mustDecodeHex(tc.RawTx)) + tx, err := parseEip1559Tx(mustDecodeHex(tc.RawTx)) if tc.ExpectErr { require.Error(t, err) continue @@ -105,21 +105,11 @@ func TestSignatures(t *testing.T) { sig, err := tx.Signature() require.Nil(t, err) - r, s, v, err := RecoverSignature(*sig) - require.Nil(t, err) - - marshaledR, err := r.MarshalJSON() - require.Nil(t, err) - - marshaledS, err := s.MarshalJSON() - require.Nil(t, err) - - marshaledV, err := v.MarshalJSON() - require.Nil(t, err) + require.NoError(t, tx.InitialiseSignature(*sig)) - require.Equal(t, tc.ExpectedR, string(marshaledR)) - require.Equal(t, tc.ExpectedS, string(marshaledS)) - require.Equal(t, tc.ExpectedV, string(marshaledV)) + require.Equal(t, tc.ExpectedR, "0x"+tx.R.Text(16)) + require.Equal(t, tc.ExpectedS, "0x"+tx.S.Text(16)) + require.Equal(t, tc.ExpectedV, "0x"+tx.V.Text(16)) } } @@ -150,6 +140,7 @@ func TestTransformParams(t *testing.T) { require.Equal(t, mustDecodeHex("0x1122334455"), evmParams.Initcode) } + func TestEcRecover(t *testing.T) { rHex := "0x479ff7fa64cf8bf641eb81635d1e8a698530d2f219951d234539e6d074819529" sHex := "0x4b6146d27be50cdbb2853ba9a42f207af8d730272f1ebe9c9a78aeef1d6aa924" @@ -233,7 +224,7 @@ func prepareTxTestcases() ([]TxTestcase, error) { res := []TxTestcase{} for _, tc := range testcases { - tx := EthTxArgs{} + tx := Eth1559TxArgs{} err := json.Unmarshal([]byte(tc.Output), &tx) if err != nil { return nil, err diff --git a/venus-shared/actors/types/eth_legacy_155_transactions.go b/venus-shared/actors/types/eth_legacy_155_transactions.go new file mode 100644 index 0000000000..db0daacfc3 --- /dev/null +++ b/venus-shared/actors/types/eth_legacy_155_transactions.go @@ -0,0 +1,300 @@ +package types + +import ( + "fmt" + + "github.com/filecoin-project/go-address" + "github.com/filecoin-project/go-state-types/big" + typescrypto "github.com/filecoin-project/go-state-types/crypto" +) + +var _ EthTransaction = (*EthLegacy155TxArgs)(nil) + +// EthLegacy155TxArgs is a legacy Ethereum transaction that uses the EIP-155 chain replay protection mechanism +// by incorporating the chainId in the signature. +// See how the `V` value in the signature is derived from the chainId at +// https://github.com/ethereum/go-ethereum/blob/86a1f0c39494c8f5caddf6bd9fbddd4bdfa944fd/core/types/transaction_signing.go#L424 +// For EthLegacy155TxArgs, the digest that is used to create a signed transaction includes the `ChainID` but the serialised RLP transaction +// does not include the `ChainID` as an explicit field. Instead, the `ChainID` is included in the V value of the signature as mentioned above. +type EthLegacy155TxArgs struct { + legacyTx *EthLegacyHomesteadTxArgs +} + +func NewEthLegacy155TxArgs(tx *EthLegacyHomesteadTxArgs) *EthLegacy155TxArgs { + return &EthLegacy155TxArgs{legacyTx: tx} +} + +func (tx *EthLegacy155TxArgs) GetLegacyTx() *EthLegacyHomesteadTxArgs { + return tx.legacyTx +} + +func (tx *EthLegacy155TxArgs) ToEthTx(smsg *SignedMessage) (EthTx, error) { + from, err := EthAddressFromFilecoinAddress(smsg.Message.From) + if err != nil { + return EthTx{}, fmt.Errorf("sender was not an eth account") + } + hash, err := tx.TxHash() + if err != nil { + return EthTx{}, fmt.Errorf("failed to get tx hash: %w", err) + } + + gasPrice := EthBigInt(tx.legacyTx.GasPrice) + ethTx := EthTx{ + ChainID: EthUint64(Eip155ChainID), + Type: EthLegacyTxType, + Nonce: EthUint64(tx.legacyTx.Nonce), + Hash: hash, + To: tx.legacyTx.To, + Value: EthBigInt(tx.legacyTx.Value), + Input: tx.legacyTx.Input, + Gas: EthUint64(tx.legacyTx.GasLimit), + GasPrice: &gasPrice, + From: from, + R: EthBigInt(tx.legacyTx.R), + S: EthBigInt(tx.legacyTx.S), + V: EthBigInt(tx.legacyTx.V), + } + + return ethTx, nil +} + +func (tx *EthLegacy155TxArgs) ToUnsignedFilecoinMessage(from address.Address) (*Message, error) { + if err := validateEIP155ChainId(tx.legacyTx.V); err != nil { + return nil, fmt.Errorf("failed to validate EIP155 chain id: %w", err) + } + return tx.legacyTx.ToUnsignedFilecoinMessage(from) +} + +func (tx *EthLegacy155TxArgs) ToRlpUnsignedMsg() ([]byte, error) { + return toRlpUnsignedMsg(tx) +} + +func (tx *EthLegacy155TxArgs) TxHash() (EthHash, error) { + encoded, err := tx.ToRawTxBytesSigned() + if err != nil { + return EthHash{}, fmt.Errorf("failed to encode rlp signed msg: %w", err) + } + + return EthHashFromTxBytes(encoded), nil +} + +func (tx *EthLegacy155TxArgs) ToRawTxBytesSigned() ([]byte, error) { + packed1, err := tx.packTxFields() + if err != nil { + return nil, err + } + + packed1 = packed1[:len(packed1)-3] // remove chainId, r and s as they are only used for signature verification + + packed2, err := packSigFields(tx.legacyTx.V, tx.legacyTx.R, tx.legacyTx.S) + if err != nil { + return nil, err + } + + encoded, err := EncodeRLP(append(packed1, packed2...)) + if err != nil { + return nil, fmt.Errorf("failed to encode rlp signed msg: %w", err) + } + return encoded, nil +} + +func (tx *EthLegacy155TxArgs) ToRlpSignedMsg() ([]byte, error) { + return toRlpSignedMsg(tx, tx.legacyTx.V, tx.legacyTx.R, tx.legacyTx.S) +} + +func (tx *EthLegacy155TxArgs) Signature() (*typescrypto.Signature, error) { + if err := validateEIP155ChainId(tx.legacyTx.V); err != nil { + return nil, fmt.Errorf("failed to validate EIP155 chain id: %w", err) + } + r := tx.legacyTx.R.Int.Bytes() + s := tx.legacyTx.S.Int.Bytes() + v := tx.legacyTx.V.Int.Bytes() + + sig := append([]byte{}, padLeadingZeros(r, 32)...) + sig = append(sig, padLeadingZeros(s, 32)...) + sig = append(sig, v...) + + // pre-pend a one byte marker so nodes know that this is a legacy transaction + sig = append([]byte{EthLegacy155TxSignaturePrefix}, sig...) + + if len(sig) != EthLegacy155TxSignatureLen0 && len(sig) != EthLegacy155TxSignatureLen1 { + return nil, fmt.Errorf("signature is not %d OR %d bytes; it is %d bytes", EthLegacy155TxSignatureLen0, EthLegacy155TxSignatureLen1, + len(sig)) + } + + return &typescrypto.Signature{ + Type: typescrypto.SigTypeDelegated, Data: sig, + }, nil +} + +func (tx *EthLegacy155TxArgs) Sender() (address.Address, error) { + if err := validateEIP155ChainId(tx.legacyTx.V); err != nil { + return address.Address{}, fmt.Errorf("failed to validate EIP155 chain id: %w", err) + } + return sender(tx) +} + +func (tx *EthLegacy155TxArgs) Type() int { + return EthLegacyTxType +} + +var big8 = big.NewInt(8) + +func (tx *EthLegacy155TxArgs) ToVerifiableSignature(sig []byte) ([]byte, error) { + if len(sig) != EthLegacy155TxSignatureLen0 && len(sig) != EthLegacy155TxSignatureLen1 { + return nil, fmt.Errorf("signature should be %d or %d bytes long but got %d bytes", + EthLegacy155TxSignatureLen0, EthLegacy155TxSignatureLen1, len(sig)) + } + if sig[0] != EthLegacy155TxSignaturePrefix { + return nil, fmt.Errorf("expected signature prefix 0x%x, but got 0x%x", EthLegacy155TxSignaturePrefix, sig[0]) + } + + // Remove the prefix byte as it's only used for legacy transaction identification + sig = sig[1:] + + // Extract the 'v' value from the signature + vValue := big.NewFromGo(big.NewInt(0).SetBytes(sig[64:])) + + if err := validateEIP155ChainId(vValue); err != nil { + return nil, fmt.Errorf("failed to validate EIP155 chain id: %w", err) + } + + // See https://github.com/ethereum/go-ethereum/blob/86a1f0c39494c8f5caddf6bd9fbddd4bdfa944fd/core/types/transaction_signing.go#L424 + chainIdMul := big.Mul(big.NewIntUnsigned(uint64(Eip155ChainID)), big.NewInt(2)) + vValue = big.Sub(vValue, chainIdMul) + vValue = big.Sub(vValue, big8) + + // Adjust 'v' value for compatibility with new transactions: 27 -> 0, 28 -> 1 + if vValue.Equals(big.NewInt(27)) { + sig[64] = 0 + } else if vValue.Equals(big.NewInt(28)) { + sig[64] = 1 + } else { + return nil, fmt.Errorf("invalid 'v' value: expected 27 or 28, got %d", vValue.Int64()) + } + + return sig[0:65], nil +} + +func (tx *EthLegacy155TxArgs) InitialiseSignature(sig typescrypto.Signature) error { + if sig.Type != typescrypto.SigTypeDelegated { + return fmt.Errorf("RecoverSignature only supports Delegated signature") + } + + if len(sig.Data) != EthLegacy155TxSignatureLen0 && len(sig.Data) != EthLegacy155TxSignatureLen1 { + return fmt.Errorf("signature should be %d or %d bytes long, but got %d bytes", EthLegacy155TxSignatureLen0, + EthLegacy155TxSignatureLen1, len(sig.Data)) + } + + if sig.Data[0] != EthLegacy155TxSignaturePrefix { + return fmt.Errorf("expected signature prefix 0x01, but got 0x%x", sig.Data[0]) + } + + // ignore the first byte of the signature as it's only used for legacy transaction identification + r_, err := parseBigInt(sig.Data[1:33]) + if err != nil { + return fmt.Errorf("cannot parse r into EthBigInt: %w", err) + } + + s_, err := parseBigInt(sig.Data[33:65]) + if err != nil { + return fmt.Errorf("cannot parse s into EthBigInt: %w", err) + } + + v_, err := parseBigInt(sig.Data[65:]) + if err != nil { + return fmt.Errorf("cannot parse v into EthBigInt: %w", err) + } + + if err := validateEIP155ChainId(v_); err != nil { + return fmt.Errorf("failed to validate EIP155 chain id: %w", err) + } + + tx.legacyTx.R = r_ + tx.legacyTx.S = s_ + tx.legacyTx.V = v_ + return nil +} + +func (tx *EthLegacy155TxArgs) packTxFields() ([]interface{}, error) { + nonce, err := formatInt(tx.legacyTx.Nonce) + if err != nil { + return nil, err + } + + // format gas price + gasPrice, err := formatBigInt(tx.legacyTx.GasPrice) + if err != nil { + return nil, err + } + + gasLimit, err := formatInt(tx.legacyTx.GasLimit) + if err != nil { + return nil, err + } + + value, err := formatBigInt(tx.legacyTx.Value) + if err != nil { + return nil, err + } + + chainIdBigInt := big.NewIntUnsigned(uint64(Eip155ChainID)) + chainId, err := formatBigInt(chainIdBigInt) + if err != nil { + return nil, err + } + + r, err := formatInt(0) + if err != nil { + return nil, err + } + + s, err := formatInt(0) + if err != nil { + return nil, err + } + + res := []interface{}{ + nonce, + gasPrice, + gasLimit, + formatEthAddr(tx.legacyTx.To), + value, + tx.legacyTx.Input, + chainId, + r, s, + } + return res, nil +} + +func validateEIP155ChainId(v big.Int) error { + chainId := deriveEIP155ChainId(v) + if !chainId.Equals(big.NewIntUnsigned(uint64(Eip155ChainID))) { + return fmt.Errorf("invalid chain id, expected %d, got %s", Eip155ChainID, chainId.String()) + } + return nil +} + +// deriveEIP155ChainId derives the chain id from the given v parameter +func deriveEIP155ChainId(v big.Int) big.Int { + if big.BitLen(v) <= 64 { + vUint64 := v.Uint64() + if vUint64 == 27 || vUint64 == 28 { + return big.NewInt(0) + } + return big.NewIntUnsigned((vUint64 - 35) / 2) + } + + v = big.Sub(v, big.NewInt(35)) + return big.Div(v, big.NewInt(2)) +} + +func calcEIP155TxSignatureLen(chain uint64, v int) int { + chainId := big.NewIntUnsigned(chain) + vVal := big.Add(big.Mul(chainId, big.NewInt(2)), big.NewInt(int64(v))) + vLen := len(vVal.Int.Bytes()) + + // EthLegacyHomesteadTxSignatureLen includes the 1 byte legacy tx marker prefix and also 1 byte for the V value. + // So we subtract 1 to not double count the length of the v value + return EthLegacyHomesteadTxSignatureLen + vLen - 1 +} diff --git a/venus-shared/actors/types/eth_legacy_155_transactions_test.go b/venus-shared/actors/types/eth_legacy_155_transactions_test.go new file mode 100644 index 0000000000..3ebdd938b7 --- /dev/null +++ b/venus-shared/actors/types/eth_legacy_155_transactions_test.go @@ -0,0 +1,188 @@ +package types + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/filecoin-project/go-state-types/big" + builtintypes "github.com/filecoin-project/go-state-types/builtin" +) + +func TestEIP155Tx(t *testing.T) { + txStr := "f86680843b9aca00835dc1ba94c2dca9a18d4a4057921d1bcb22da05e68e46b1d06480820297a0f91ee69c4603c4f21131467ee9e06ad4a96d0a29fa8064db61f3adaea0eb6e92a07181e306bb8f773d94cc3b75e9835de00c004e072e6630c0c46971d38706bb01" + + bz := mustDecodeHex(txStr) + + tx, err := parseLegacyTx(bz) + require.NoError(t, err) + + eth155Tx, ok := tx.(*EthLegacy155TxArgs) + require.True(t, ok) + + // Verify nonce + require.EqualValues(t, 0, eth155Tx.legacyTx.Nonce) + + // Verify recipient address + expectedToAddr, err := ParseEthAddress("0xc2dca9a18d4a4057921d1bcb22da05e68e46b1d0") + require.NoError(t, err) + require.EqualValues(t, expectedToAddr, *eth155Tx.legacyTx.To) + + // Verify sender address + expectedFromAddr, err := ParseEthAddress("0xA2BBB73aC59b256415e91A820b224dbAF2C268FA") + require.NoError(t, err) + sender, err := eth155Tx.Sender() + require.NoError(t, err) + expectedFromFilecoinAddr, err := expectedFromAddr.ToFilecoinAddress() + require.NoError(t, err) + require.EqualValues(t, expectedFromFilecoinAddr, sender) + + // Verify transaction value + expectedValue, ok := big.NewInt(0).SetString("100", 10) + require.True(t, ok) + require.True(t, eth155Tx.legacyTx.Value.Cmp(expectedValue) == 0) + + // Verify gas limit and gas price + expectedGasPrice, ok := big.NewInt(0).SetString("1000000000", 10) + require.True(t, ok) + require.EqualValues(t, 6144442, eth155Tx.legacyTx.GasLimit) + require.True(t, eth155Tx.legacyTx.GasPrice.Cmp(expectedGasPrice) == 0) + + require.Empty(t, eth155Tx.legacyTx.Input) + + // Verify signature values (v, r, s) + expectedV, ok := big.NewInt(0).SetString("0297", 16) + require.True(t, ok) + require.True(t, eth155Tx.legacyTx.V.Cmp(expectedV) == 0) + + expectedR, ok := big.NewInt(0).SetString("f91ee69c4603c4f21131467ee9e06ad4a96d0a29fa8064db61f3adaea0eb6e92", 16) + require.True(t, ok) + require.True(t, eth155Tx.legacyTx.R.Cmp(expectedR) == 0) + + expectedS, ok := big.NewInt(0).SetString("7181e306bb8f773d94cc3b75e9835de00c004e072e6630c0c46971d38706bb01", 16) + require.True(t, ok) + require.True(t, eth155Tx.legacyTx.S.Cmp(expectedS) == 0) + + // Convert to signed Filecoin message and verify fields + smsg, err := ToSignedFilecoinMessage(eth155Tx) + require.NoError(t, err) + + require.EqualValues(t, smsg.Message.From, sender) + + expectedToFilecoinAddr, err := eth155Tx.legacyTx.To.ToFilecoinAddress() + require.NoError(t, err) + require.EqualValues(t, smsg.Message.To, expectedToFilecoinAddr) + require.EqualValues(t, smsg.Message.Value, eth155Tx.legacyTx.Value) + require.EqualValues(t, smsg.Message.GasLimit, eth155Tx.legacyTx.GasLimit) + require.EqualValues(t, smsg.Message.GasFeeCap, eth155Tx.legacyTx.GasPrice) + require.EqualValues(t, smsg.Message.GasPremium, eth155Tx.legacyTx.GasPrice) + require.EqualValues(t, smsg.Message.Nonce, eth155Tx.legacyTx.Nonce) + require.Empty(t, smsg.Message.Params) + require.EqualValues(t, smsg.Message.Method, builtintypes.MethodsEVM.InvokeContract) + + // Convert signed Filecoin message back to Ethereum transaction and verify equality + ethTx, err := EthTransactionFromSignedFilecoinMessage(smsg) + require.NoError(t, err) + convertedLegacyTx, ok := ethTx.(*EthLegacy155TxArgs) + require.True(t, ok) + eth155Tx.legacyTx.Input = nil + require.EqualValues(t, convertedLegacyTx, eth155Tx) + + // Verify EthTx fields + ethTxVal, err := eth155Tx.ToEthTx(smsg) + require.NoError(t, err) + expectedHash, err := eth155Tx.TxHash() + require.NoError(t, err) + require.EqualValues(t, ethTxVal.Hash, expectedHash) + require.Nil(t, ethTxVal.MaxFeePerGas) + require.Nil(t, ethTxVal.MaxPriorityFeePerGas) + require.EqualValues(t, ethTxVal.Gas, eth155Tx.legacyTx.GasLimit) + require.EqualValues(t, ethTxVal.Value, eth155Tx.legacyTx.Value) + require.EqualValues(t, ethTxVal.Nonce, eth155Tx.legacyTx.Nonce) + require.EqualValues(t, ethTxVal.To, eth155Tx.legacyTx.To) + require.EqualValues(t, ethTxVal.From, expectedFromAddr) +} + +func TestDeriveEIP155ChainId(t *testing.T) { + tests := []struct { + name string + v big.Int + expectedChainId big.Int + }{ + { + name: "V equals 27", + v: big.NewInt(27), + expectedChainId: big.NewInt(0), + }, + { + name: "V equals 28", + v: big.NewInt(28), + expectedChainId: big.NewInt(0), + }, + { + name: "V small chain ID", + v: big.NewInt(37), // (37 - 35) / 2 = 1 + expectedChainId: big.NewInt(1), + }, + { + name: "V large chain ID", + v: big.NewInt(1001), // (1001 - 35) / 2 = 483 + expectedChainId: big.NewInt(483), + }, + { + name: "V very large chain ID", + v: big.NewInt(1 << 20), // (1048576 - 35) / 2 = 524770 + expectedChainId: big.NewInt(524270), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := deriveEIP155ChainId(tt.v) + require.True(t, result.Equals(tt.expectedChainId), "Expected %s, got %s for V=%s", tt.expectedChainId.String(), result.String(), tt.v.String()) + }) + } +} + +func TestCalcEIP155TxSignatureLen(t *testing.T) { + tests := []struct { + name string + chainID uint64 + expected int + }{ + { + name: "ChainID that fits in 1 byte", + chainID: 0x01, + expected: EthLegacyHomesteadTxSignatureLen + 1 - 1, + }, + { + name: "ChainID that fits in 2 bytes", + chainID: 0x0100, + expected: EthLegacyHomesteadTxSignatureLen + 2 - 1, + }, + { + name: "ChainID that fits in 3 bytes", + chainID: 0x010000, + expected: EthLegacyHomesteadTxSignatureLen + 3 - 1, + }, + { + name: "ChainID that fits in 4 bytes", + chainID: 0x01000000, + expected: EthLegacyHomesteadTxSignatureLen + 4 - 1, + }, + { + name: "ChainID that fits in 6 bytes", + chainID: 0x010000000000, + expected: EthLegacyHomesteadTxSignatureLen + 6 - 1, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := calcEIP155TxSignatureLen(tt.chainID, 1) + if result != tt.expected { + t.Errorf("calcEIP155TxSignatureLen(%d) = %d, want %d", tt.chainID, result, tt.expected) + } + }) + } +} diff --git a/venus-shared/actors/types/eth_legacy_homestead_transactions.go b/venus-shared/actors/types/eth_legacy_homestead_transactions.go new file mode 100644 index 0000000000..ef96b52674 --- /dev/null +++ b/venus-shared/actors/types/eth_legacy_homestead_transactions.go @@ -0,0 +1,226 @@ +package types + +import ( + "fmt" + + "golang.org/x/xerrors" + + "github.com/filecoin-project/go-address" + "github.com/filecoin-project/go-state-types/big" + typescrypto "github.com/filecoin-project/go-state-types/crypto" +) + +var _ EthTransaction = (*EthLegacyHomesteadTxArgs)(nil) + +type EthLegacyHomesteadTxArgs struct { + Nonce int `json:"nonce"` + GasPrice big.Int `json:"gasPrice"` + GasLimit int `json:"gasLimit"` + To *EthAddress `json:"to"` + Value big.Int `json:"value"` + Input []byte `json:"input"` + V big.Int `json:"v"` + R big.Int `json:"r"` + S big.Int `json:"s"` +} + +func (tx *EthLegacyHomesteadTxArgs) ToEthTx(smsg *SignedMessage) (EthTx, error) { + from, err := EthAddressFromFilecoinAddress(smsg.Message.From) + if err != nil { + return EthTx{}, fmt.Errorf("sender was not an eth account") + } + hash, err := tx.TxHash() + if err != nil { + return EthTx{}, fmt.Errorf("failed to get tx hash: %w", err) + } + + gasPrice := EthBigInt(tx.GasPrice) + ethTx := EthTx{ + ChainID: EthLegacyHomesteadTxChainID, + Type: EthLegacyTxType, + Nonce: EthUint64(tx.Nonce), + Hash: hash, + To: tx.To, + Value: EthBigInt(tx.Value), + Input: tx.Input, + Gas: EthUint64(tx.GasLimit), + GasPrice: &gasPrice, + From: from, + R: EthBigInt(tx.R), + S: EthBigInt(tx.S), + V: EthBigInt(tx.V), + } + + return ethTx, nil +} + +func (tx *EthLegacyHomesteadTxArgs) ToUnsignedFilecoinMessage(from address.Address) (*Message, error) { + mi, err := getFilecoinMethodInfo(tx.To, tx.Input) + if err != nil { + return nil, xerrors.Errorf("failed to get method info: %w", err) + } + + return &Message{ + Version: 0, + To: mi.to, + From: from, + Nonce: uint64(tx.Nonce), + Value: tx.Value, + GasLimit: int64(tx.GasLimit), + GasFeeCap: tx.GasPrice, + GasPremium: tx.GasPrice, + Method: mi.method, + Params: mi.params, + }, nil +} + +func (tx *EthLegacyHomesteadTxArgs) ToVerifiableSignature(sig []byte) ([]byte, error) { + if len(sig) != EthLegacyHomesteadTxSignatureLen { + return nil, fmt.Errorf("signature should be %d bytes long (1 byte metadata, %d bytes sig data), but got %d bytes", + EthLegacyHomesteadTxSignatureLen, EthLegacyHomesteadTxSignatureLen-1, len(sig)) + } + if sig[0] != EthLegacyHomesteadTxSignaturePrefix { + return nil, fmt.Errorf("expected signature prefix 0x%x, but got 0x%x", EthLegacyHomesteadTxSignaturePrefix, sig[0]) + } + + // Remove the prefix byte as it's only used for legacy transaction identification + sig = sig[1:] + + // Extract the 'v' value from the signature, which is the last byte in Ethereum signatures + vValue := big.NewFromGo(big.NewInt(0).SetBytes(sig[64:])) + + // Adjust 'v' value for compatibility with new transactions: 27 -> 0, 28 -> 1 + if vValue.Equals(big.NewInt(27)) { + sig[64] = 0 + } else if vValue.Equals(big.NewInt(28)) { + sig[64] = 1 + } else { + return nil, fmt.Errorf("invalid 'v' value: expected 27 or 28, got %d", vValue.Int64()) + } + + return sig, nil +} + +func (tx *EthLegacyHomesteadTxArgs) ToRlpUnsignedMsg() ([]byte, error) { + return toRlpUnsignedMsg(tx) +} + +func (tx *EthLegacyHomesteadTxArgs) TxHash() (EthHash, error) { + rlp, err := tx.ToRlpSignedMsg() + if err != nil { + return EthHash{}, err + } + return EthHashFromTxBytes(rlp), nil +} + +func (tx *EthLegacyHomesteadTxArgs) ToRlpSignedMsg() ([]byte, error) { + return toRlpSignedMsg(tx, tx.V, tx.R, tx.S) +} + +func (tx *EthLegacyHomesteadTxArgs) Signature() (*typescrypto.Signature, error) { + // throw an error if the v value is not 27 or 28 + if !tx.V.Equals(big.NewInt(27)) && !tx.V.Equals(big.NewInt(28)) { + return nil, fmt.Errorf("legacy homestead transactions only support 27 or 28 for v") + } + r := tx.R.Int.Bytes() + s := tx.S.Int.Bytes() + v := tx.V.Int.Bytes() + + sig := append([]byte{}, padLeadingZeros(r, 32)...) + sig = append(sig, padLeadingZeros(s, 32)...) + if len(v) == 0 { + sig = append(sig, 0) + } else { + sig = append(sig, v[0]) + } + // pre-pend a one byte marker so nodes know that this is a legacy transaction + sig = append([]byte{EthLegacyHomesteadTxSignaturePrefix}, sig...) + + if len(sig) != EthLegacyHomesteadTxSignatureLen { + return nil, fmt.Errorf("signature is not %d bytes", EthLegacyHomesteadTxSignatureLen) + } + + return &typescrypto.Signature{ + Type: typescrypto.SigTypeDelegated, Data: sig, + }, nil +} + +func (tx *EthLegacyHomesteadTxArgs) Sender() (address.Address, error) { + return sender(tx) +} + +func (tx *EthLegacyHomesteadTxArgs) Type() int { + return EthLegacyTxType +} + +func (tx *EthLegacyHomesteadTxArgs) InitialiseSignature(sig typescrypto.Signature) error { + if sig.Type != typescrypto.SigTypeDelegated { + return fmt.Errorf("RecoverSignature only supports Delegated signature") + } + + if len(sig.Data) != EthLegacyHomesteadTxSignatureLen { + return fmt.Errorf("signature should be %d bytes long, but got %d bytes", EthLegacyHomesteadTxSignatureLen, len(sig.Data)) + } + + if sig.Data[0] != EthLegacyHomesteadTxSignaturePrefix { + return fmt.Errorf("expected signature prefix 0x01, but got 0x%x", sig.Data[0]) + } + + // ignore the first byte of the signature as it's only used for legacy transaction identification + r_, err := parseBigInt(sig.Data[1:33]) + if err != nil { + return fmt.Errorf("cannot parse r into EthBigInt: %w", err) + } + + s_, err := parseBigInt(sig.Data[33:65]) + if err != nil { + return fmt.Errorf("cannot parse s into EthBigInt: %w", err) + } + + v_, err := parseBigInt([]byte{sig.Data[65]}) + if err != nil { + return fmt.Errorf("cannot parse v into EthBigInt: %w", err) + } + + if !v_.Equals(big.NewInt(27)) && !v_.Equals(big.NewInt(28)) { + return fmt.Errorf("legacy homestead transactions only support 27 or 28 for v") + } + + tx.R = r_ + tx.S = s_ + tx.V = v_ + return nil +} + +func (tx *EthLegacyHomesteadTxArgs) packTxFields() ([]interface{}, error) { + nonce, err := formatInt(tx.Nonce) + if err != nil { + return nil, err + } + + // format gas price + gasPrice, err := formatBigInt(tx.GasPrice) + if err != nil { + return nil, err + } + + gasLimit, err := formatInt(tx.GasLimit) + if err != nil { + return nil, err + } + + value, err := formatBigInt(tx.Value) + if err != nil { + return nil, err + } + + res := []interface{}{ + nonce, + gasPrice, + gasLimit, + formatEthAddr(tx.To), + value, + tx.Input, + } + return res, nil +} diff --git a/venus-shared/actors/types/eth_legacy_homestead_transactions_test.go b/venus-shared/actors/types/eth_legacy_homestead_transactions_test.go new file mode 100644 index 0000000000..3134f0aab2 --- /dev/null +++ b/venus-shared/actors/types/eth_legacy_homestead_transactions_test.go @@ -0,0 +1,299 @@ +package types + +import ( + "encoding/hex" + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/filecoin-project/go-state-types/big" + builtintypes "github.com/filecoin-project/go-state-types/builtin" + + "github.com/filecoin-project/venus/pkg/crypto" +) + +func TestEthLegacyHomesteadTxArgs(t *testing.T) { + testcases := []struct { + RawTx string + ExpectedNonce uint64 + ExpectedTo string + ExpectedInput string + ExpectedGasPrice big.Int + ExpectedGasLimit int + ExpectErr bool + }{ + { + "0xf882800182540894095e7baea6a6c7c4c2dfeb977efac326af552d8780a3deadbeef0000000101010010101010101010101010101aaabbbbbbcccccccddddddddd1ba048b55bfa915ac795c431978d8a6a992b628d557da5ff759b307d495a36649353a01fffd310ac743f371de3b9f7f9cb56c0b28ad43601b4ab949f53faa07bd2c804", + 0x0, + "0x095e7baea6a6c7c4c2dfeb977efac326af552d87", + "0xdeadbeef0000000101010010101010101010101010101aaabbbbbbcccccccddddddddd", + big.NewInt(1), + 0x5408, + false, + }, + { + "0xf85f030182520794b94f5374fce5edbc8e2a8697c15331677e6ebf0b0a801ba098ff921201554726367d2be8c804a7ff89ccf285ebc57dff8ae4c44b9c19ac4aa07778cde41a8a37f6a087622b38bc201bd3e7df06dce067569d4def1b53dba98c", + 0x3, + "0xb94f5374fce5edbc8e2a8697c15331677e6ebf0b", + "0x", + big.NewInt(1), + 0x5207, + false, + }, + } + + for i, tc := range testcases { + // parse txargs + tx, err := parseLegacyTx(mustDecodeHex(tc.RawTx)) + require.NoError(t, err) + + msgRecovered, err := tx.ToRlpUnsignedMsg() + require.NoError(t, err) + + // verify signatures + from, err := tx.Sender() + require.NoError(t, err) + + smsg, err := ToSignedFilecoinMessage(tx) + require.NoError(t, err) + + sig := smsg.Signature.Data[:] + sig = sig[1:] + vValue := big.NewInt(0).SetBytes(sig[64:]) + vValue_ := big.Sub(big.NewFromGo(vValue), big.NewInt(27)) + sig[64] = byte(vValue_.Uint64()) + smsg.Signature.Data = sig + + err = crypto.Verify(&smsg.Signature, from, msgRecovered) + require.NoError(t, err) + + txArgs := tx.(*EthLegacyHomesteadTxArgs) + // verify data + require.EqualValues(t, tc.ExpectedNonce, txArgs.Nonce, i) + + expectedTo, err := ParseEthAddress(tc.ExpectedTo) + require.NoError(t, err) + require.EqualValues(t, expectedTo, *txArgs.To, i) + require.EqualValues(t, tc.ExpectedInput, "0x"+hex.EncodeToString(txArgs.Input)) + require.EqualValues(t, tc.ExpectedGasPrice, txArgs.GasPrice) + require.EqualValues(t, tc.ExpectedGasLimit, txArgs.GasLimit) + } +} + +func TestLegacyHomesteadSignatures(t *testing.T) { + testcases := []struct { + RawTx string + ExpectedR string + ExpectedS string + ExpectedV string + ExpectErr bool + ExpectErrMsg string + ExpectVMismatch bool + }{ + { + "0xf882800182540894095e7baea6a6c7c4c2dfeb977efac326af552d8780a3deadbeef0000000101010010101010101010101010101aaabbbbbbcccccccddddddddd1ba048b55bfa915ac795c431978d8a6a992b628d557da5ff759b307d495a36649353a01fffd310ac743f371de3b9f7f9cb56c0b28ad43601b4ab949f53faa07bd2c804", + "0x48b55bfa915ac795c431978d8a6a992b628d557da5ff759b307d495a36649353", + "0x1fffd310ac743f371de3b9f7f9cb56c0b28ad43601b4ab949f53faa07bd2c804", + "0x1b", + false, + "", + false, + }, + { + "0xf85f030182520794b94f5374fce5edbc8e2a8697c15331677e6ebf0b0a801ba098ff921201554726367d2be8c804a7ff89ccf285ebc57dff8ae4c44b9c19ac4aa07778cde41a8a37f6a087622b38bc201bd3e7df06dce067569d4def1b53dba98c", + "0x98ff921201554726367d2be8c804a7ff89ccf285ebc57dff8ae4c44b9c19ac4a", + "0x7778cde41a8a37f6a087622b38bc201bd3e7df06dce067569d4def1b53dba98c", + "0x1b", + false, + "", + false, + }, + { + "0xf882800182540894095e7baea6a6c7c4c2dfeb977efac326af552d8780a3deadbeef0000000101010010101010101010101010101aaabbbbbbcccccccddddddddd1ba048b55bfa915ac795c431978d8a6a992b628d557da5ff759b307d495a36649353a01fffd310ac743f371de3b9f7f9cb56c0b28ad43601b4ab949f53faa07bd2c804", + "0x48b55bfa915ac795c431978d8a6a992b628d557da5ff759b307d495a36649353", + "0x1fffd310ac743f371de3b9f7f9cb56c0b28ad43601b4ab949f53faa07bd2c804", + "0x1c", + false, + "", + true, + }, + { + "0xf882800182540894095e7baea6a6c7c4c2dfeb977efac326af552d8780a3deadbeef0000000101010010101010101010101010101aaabbbbbbcccccccddddddddd1ba048b55bfa915ac795c431978d8a6a992b628d557da5ff759b307d495a36649353a01fffd310ac743f371de3b9f7f9cb56c0b28ad43601b4ab949f53faa07bd2c804", + "0x48b55bfa915ac795c431978d8a6a992b628d557da5ff759b307d495a36649353", + "0x1fffd310ac743f371de3b9f7f9cb56c0b28ad43601b4ab949f53faa07bd2c804", + "0x1f", + false, + "", + true, + }, + { + "0x00", + "", + "", + "", + true, + "not a legacy eth transaction", + false, + }, + } + + for i, tc := range testcases { + tx, err := parseLegacyTx(mustDecodeHex(tc.RawTx)) + if tc.ExpectErr { + require.Error(t, err) + require.Contains(t, err.Error(), tc.ExpectErrMsg) + continue + } + require.Nil(t, err) + + sig, err := tx.Signature() + require.Nil(t, err) + + require.NoError(t, tx.InitialiseSignature(*sig)) + + txArgs := tx.(*EthLegacyHomesteadTxArgs) + + require.Equal(t, tc.ExpectedR, "0x"+txArgs.R.Text(16), i) + require.Equal(t, tc.ExpectedS, "0x"+txArgs.S.Text(16), i) + + if tc.ExpectVMismatch { + require.NotEqual(t, tc.ExpectedV, "0x"+txArgs.V.Text(16), i) + } else { + require.Equal(t, tc.ExpectedV, "0x"+txArgs.V.Text(16), i) + } + } +} + +// https://etherscan.io/getRawTx?tx=0xc55e2b90168af6972193c1f86fa4d7d7b31a29c156665d15b9cd48618b5177ef +// https://tools.deth.net/tx-decoder +func TestEtherScanLegacyRLP(t *testing.T) { + rlp := "0xf8718301efc58506fc23ac008305161594104994f45d9d697ca104e5704a7b77d7fec3537c890821878651a4d70000801ba051222d91a379452395d0abaff981af4cfcc242f25cfaf947dea8245a477731f9a03a997c910b4701cca5d933fb26064ee5af7fe3236ff0ef2b58aa50b25aff8ca5" + bz := mustDecodeHex(rlp) + + tx, err := parseLegacyTx(bz) + require.NoError(t, err) + + ethLegacyTx, ok := tx.(*EthLegacyHomesteadTxArgs) + require.True(t, ok) + + // Verify nonce + require.EqualValues(t, 0x1efc5, ethLegacyTx.Nonce) + + // Verify recipient address + expectedToAddr, err := ParseEthAddress("0x104994f45d9d697ca104e5704a7b77d7fec3537c") + require.NoError(t, err) + require.EqualValues(t, expectedToAddr, *ethLegacyTx.To) + + // Verify sender address + expectedFromAddr, err := ParseEthAddress("0x32Be343B94f860124dC4fEe278FDCBD38C102D88") + require.NoError(t, err) + sender, err := ethLegacyTx.Sender() + require.NoError(t, err) + expectedFromFilecoinAddr, err := expectedFromAddr.ToFilecoinAddress() + require.NoError(t, err) + require.EqualValues(t, expectedFromFilecoinAddr, sender) + + // Verify transaction value + expectedValue, ok := big.NewInt(0).SetString("821878651a4d70000", 16) + require.True(t, ok) + require.True(t, ethLegacyTx.Value.Cmp(expectedValue) == 0) + + // Verify gas limit and gas price + expectedGasPrice, ok := big.NewInt(0).SetString("6fc23ac00", 16) + require.True(t, ok) + require.EqualValues(t, 0x51615, ethLegacyTx.GasLimit) + require.True(t, ethLegacyTx.GasPrice.Cmp(expectedGasPrice) == 0) + + require.Empty(t, ethLegacyTx.Input) + + // Verify signature values (v, r, s) + expectedV, ok := big.NewInt(0).SetString("1b", 16) + require.True(t, ok) + require.True(t, ethLegacyTx.V.Cmp(expectedV) == 0) + + expectedR, ok := big.NewInt(0).SetString("51222d91a379452395d0abaff981af4cfcc242f25cfaf947dea8245a477731f9", 16) + require.True(t, ok) + require.True(t, ethLegacyTx.R.Cmp(expectedR) == 0) + + expectedS, ok := big.NewInt(0).SetString("3a997c910b4701cca5d933fb26064ee5af7fe3236ff0ef2b58aa50b25aff8ca5", 16) + require.True(t, ok) + require.True(t, ethLegacyTx.S.Cmp(expectedS) == 0) + + // Convert to signed Filecoin message and verify fields + smsg, err := ToSignedFilecoinMessage(ethLegacyTx) + require.NoError(t, err) + + require.EqualValues(t, smsg.Message.From, sender) + + expectedToFilecoinAddr, err := ethLegacyTx.To.ToFilecoinAddress() + require.NoError(t, err) + require.EqualValues(t, smsg.Message.To, expectedToFilecoinAddr) + require.EqualValues(t, smsg.Message.Value, ethLegacyTx.Value) + require.EqualValues(t, smsg.Message.GasLimit, ethLegacyTx.GasLimit) + require.EqualValues(t, smsg.Message.GasFeeCap, ethLegacyTx.GasPrice) + require.EqualValues(t, smsg.Message.GasPremium, ethLegacyTx.GasPrice) + require.EqualValues(t, smsg.Message.Nonce, ethLegacyTx.Nonce) + require.Empty(t, smsg.Message.Params) + require.EqualValues(t, smsg.Message.Method, builtintypes.MethodsEVM.InvokeContract) + + // Convert signed Filecoin message back to Ethereum transaction and verify equality + ethTx, err := EthTransactionFromSignedFilecoinMessage(smsg) + require.NoError(t, err) + convertedLegacyTx, ok := ethTx.(*EthLegacyHomesteadTxArgs) + require.True(t, ok) + ethLegacyTx.Input = nil + require.EqualValues(t, convertedLegacyTx, ethLegacyTx) + + // Verify EthTx fields + ethTxVal, err := ethLegacyTx.ToEthTx(smsg) + require.NoError(t, err) + expectedHash, err := ethLegacyTx.TxHash() + require.NoError(t, err) + require.EqualValues(t, ethTxVal.Hash, expectedHash) + require.Nil(t, ethTxVal.MaxFeePerGas) + require.Nil(t, ethTxVal.MaxPriorityFeePerGas) + require.EqualValues(t, ethTxVal.Gas, ethLegacyTx.GasLimit) + require.EqualValues(t, ethTxVal.Value, ethLegacyTx.Value) + require.EqualValues(t, ethTxVal.Nonce, ethLegacyTx.Nonce) + require.EqualValues(t, ethTxVal.To, ethLegacyTx.To) + require.EqualValues(t, ethTxVal.From, expectedFromAddr) +} + +func TestFailurePaths(t *testing.T) { + // Test case for invalid RLP + invalidRLP := "0x08718301efc58506fc23ac008305161594104994f45d9d697ca104e5704a7b77d7fec3537c890821878651a4d70000801ba051222d91a379452395d0abaff981af4cfcc242f25cfaf947dea8245a477731f9a03a997c910b4701cca5d933fb26064ee5af7fe3236ff0ef2b58aa50b25aff8ca5" + decoded, err := hex.DecodeString(strings.TrimPrefix(invalidRLP, "0x")) + require.NoError(t, err) + + _, err = parseLegacyTx(decoded) + require.Error(t, err, "Expected error for invalid RLP") + + // Test case for mangled signature + mangledSignatureRLP := "0xf8718301efc58506fc23ac008305161594104994f45d9d697ca104e5704a7b77d7fec3537c890821878651a4d70000801ba051222d91a379452395d0abaff981af4cfcc242f25cfaf947dea8245a477731f9a03a997c910b4701cca5d933fb26064ee5af7fe3236ff0ef2b58aa50b25aff8ca5" + decodedSig, err := hex.DecodeString(strings.TrimPrefix(mangledSignatureRLP, "0x")) + require.NoError(t, err) + + tx, err := parseLegacyTx(decodedSig) + require.NoError(t, err) + + ethLegacyTx, ok := tx.(*EthLegacyHomesteadTxArgs) + require.True(t, ok) + + // Mangle R value + ethLegacyTx.R = big.Add(ethLegacyTx.R, big.NewInt(1)) + + expectedFromAddr, err := ParseEthAddress("0x32Be343B94f860124dC4fEe278FDCBD38C102D88") + require.NoError(t, err) + expectedFromFilecoinAddr, err := expectedFromAddr.ToFilecoinAddress() + require.NoError(t, err) + + senderAddr, err := ethLegacyTx.Sender() + require.NoError(t, err) + require.NotEqual(t, senderAddr, expectedFromFilecoinAddr, "Expected sender address to not match after mangling R value") + + // Mangle V value + ethLegacyTx.V = big.NewInt(1) + _, err = ethLegacyTx.Sender() + require.Error(t, err, "Expected error when V value is not 27 or 28") +} diff --git a/venus-shared/types/eth_test.go b/venus-shared/actors/types/eth_test.go similarity index 100% rename from venus-shared/types/eth_test.go rename to venus-shared/actors/types/eth_test.go diff --git a/venus-shared/actors/types/eth_transactions.go b/venus-shared/actors/types/eth_transactions.go index e57b5f215e..1c04e66db4 100644 --- a/venus-shared/actors/types/eth_transactions.go +++ b/venus-shared/actors/types/eth_transactions.go @@ -3,21 +3,68 @@ package types import ( "bytes" "encoding/binary" + "errors" "fmt" mathbig "math/big" - cbg "github.com/whyrusleeping/cbor-gen" - "golang.org/x/crypto/sha3" - "github.com/filecoin-project/go-address" gocrypto "github.com/filecoin-project/go-crypto" "github.com/filecoin-project/go-state-types/abi" "github.com/filecoin-project/go-state-types/big" builtintypes "github.com/filecoin-project/go-state-types/builtin" typescrypto "github.com/filecoin-project/go-state-types/crypto" + cbg "github.com/whyrusleeping/cbor-gen" + "golang.org/x/crypto/sha3" +) + +const ( + EthLegacyTxType = 0x00 + EIP1559TxType = 0x02 +) + +const ( + EthEIP1559TxSignatureLen = 65 + EthLegacyHomesteadTxSignatureLen = 66 + EthLegacyHomesteadTxSignaturePrefix = 0x01 + EthLegacy155TxSignaturePrefix = 0x02 + EthLegacyHomesteadTxChainID = 0x00 ) -const Eip1559TxType = 2 +var ( + EthLegacy155TxSignatureLen0 int + EthLegacy155TxSignatureLen1 int +) + +func init() { + initEthLegacy155TxSignature() +} + +func initEthLegacy155TxSignature() { + EthLegacy155TxSignatureLen0 = calcEIP155TxSignatureLen(uint64(Eip155ChainID), 35) + EthLegacy155TxSignatureLen1 = calcEIP155TxSignatureLen(uint64(Eip155ChainID), 36) +} + +// EthTransaction defines the interface for Ethereum-like transactions. +// It provides methods to convert transactions to various formats, +// retrieve transaction details, and manipulate transaction signatures. +type EthTransaction interface { + Type() int + Sender() (address.Address, error) + Signature() (*typescrypto.Signature, error) + InitialiseSignature(sig typescrypto.Signature) error + ToUnsignedFilecoinMessage(from address.Address) (*Message, error) + ToRlpUnsignedMsg() ([]byte, error) + ToRlpSignedMsg() ([]byte, error) + TxHash() (EthHash, error) + ToVerifiableSignature(sig []byte) ([]byte, error) + ToEthTx(*SignedMessage) (EthTx, error) +} + +// EthTx represents an Ethereum transaction structure, encapsulating fields that align with the standard Ethereum transaction components. +// This structure can represent both EIP-1559 transactions and legacy Homestead transactions: +// - In EIP-1559 transactions, the `GasPrice` field is set to nil/empty. +// - In legacy Homestead transactions, the `GasPrice` field is populated to specify the fee per unit of gas, while the `MaxFeePerGas` and `MaxPriorityFeePerGas` fields are set to nil/empty. +// Additionally, both the `ChainID` and the `Type` fields are set to 0 in legacy Homestead transactions to differentiate them from EIP-1559 transactions. type EthTx struct { ChainID EthUint64 `json:"chainId"` @@ -32,169 +79,122 @@ type EthTx struct { Type EthUint64 `json:"type"` Input EthBytes `json:"input"` Gas EthUint64 `json:"gas"` - MaxFeePerGas EthBigInt `json:"maxFeePerGas"` - MaxPriorityFeePerGas EthBigInt `json:"maxPriorityFeePerGas"` + MaxFeePerGas *EthBigInt `json:"maxFeePerGas,omitempty"` + MaxPriorityFeePerGas *EthBigInt `json:"maxPriorityFeePerGas,omitempty"` + GasPrice *EthBigInt `json:"gasPrice,omitempty"` AccessList []EthHash `json:"accessList"` V EthBigInt `json:"v"` R EthBigInt `json:"r"` S EthBigInt `json:"s"` } -type EthTxArgs struct { - ChainID int `json:"chainId"` - Nonce int `json:"nonce"` - To *EthAddress `json:"to"` - Value big.Int `json:"value"` - MaxFeePerGas big.Int `json:"maxFeePerGas"` - MaxPriorityFeePerGas big.Int `json:"maxPriorityFeePerGas"` - GasLimit int `json:"gasLimit"` - Input []byte `json:"input"` - V big.Int `json:"v"` - R big.Int `json:"r"` - S big.Int `json:"s"` +func (tx *EthTx) GasFeeCap() (EthBigInt, error) { + if tx.GasPrice == nil && tx.MaxFeePerGas == nil { + return EthBigInt{}, fmt.Errorf("gas fee cap is not set") + } + if tx.MaxFeePerGas != nil { + return *tx.MaxFeePerGas, nil + } + return *tx.GasPrice, nil } -// EthTxFromSignedEthMessage does NOT populate: -// - BlockHash -// - BlockNumber -// - TransactionIndex -// - From -// - Hash -func EthTxFromSignedEthMessage(smsg *SignedMessage) (EthTx, error) { - if smsg.Signature.Type != typescrypto.SigTypeDelegated { - return EthTx{}, fmt.Errorf("signature is not delegated type, is type: %d", smsg.Signature.Type) +func (tx *EthTx) GasPremium() (EthBigInt, error) { + if tx.GasPrice == nil && tx.MaxPriorityFeePerGas == nil { + return EthBigInt{}, fmt.Errorf("gas premium is not set") } - txArgs, err := EthTxArgsFromUnsignedEthMessage(&smsg.Message) - if err != nil { - return EthTx{}, fmt.Errorf("failed to convert the unsigned message: %w", err) + if tx.MaxPriorityFeePerGas != nil { + return *tx.MaxPriorityFeePerGas, nil } - r, s, v, err := RecoverSignature(smsg.Signature) - if err != nil { - return EthTx{}, fmt.Errorf("failed to recover signature: %w", err) - } - - return EthTx{ - Nonce: EthUint64(txArgs.Nonce), - ChainID: EthUint64(txArgs.ChainID), - To: txArgs.To, - Value: EthBigInt(txArgs.Value), - Type: Eip1559TxType, - Gas: EthUint64(txArgs.GasLimit), - MaxFeePerGas: EthBigInt(txArgs.MaxFeePerGas), - MaxPriorityFeePerGas: EthBigInt(txArgs.MaxPriorityFeePerGas), - AccessList: []EthHash{}, - V: v, - R: r, - S: s, - Input: txArgs.Input, - }, nil + return *tx.GasPrice, nil } -func EthTxArgsFromUnsignedEthMessage(msg *Message) (EthTxArgs, error) { - var ( - to *EthAddress - params []byte - err error - ) - - if msg.Version != 0 { - return EthTxArgs{}, fmt.Errorf("unsupported msg version: %d", msg.Version) +func EthTransactionFromSignedFilecoinMessage(smsg *SignedMessage) (EthTransaction, error) { + if smsg == nil { + return nil, errors.New("signed message is nil") } - if len(msg.Params) > 0 { - paramsReader := bytes.NewReader(msg.Params) - params, err = cbg.ReadByteArray(paramsReader, uint64(len(msg.Params))) - if err != nil { - return EthTxArgs{}, fmt.Errorf("failed to read params byte array: %w", err) - } - if paramsReader.Len() != 0 { - return EthTxArgs{}, fmt.Errorf("extra data found in params") - } - if len(params) == 0 { - return EthTxArgs{}, fmt.Errorf("non-empty params encode empty byte array") - } + // Ensure the signature type is delegated. + if smsg.Signature.Type != typescrypto.SigTypeDelegated { + return nil, fmt.Errorf("signature is not delegated type, is type: %d", smsg.Signature.Type) } - if msg.To == builtintypes.EthereumAddressManagerActorAddr { - if msg.Method != builtintypes.MethodsEAM.CreateExternal { - return EthTxArgs{}, fmt.Errorf("unsupported EAM method") - } - } else if msg.Method == builtintypes.MethodsEVM.InvokeContract { - addr, err := EthAddressFromFilecoinAddress(msg.To) - if err != nil { - return EthTxArgs{}, err - } - to = &addr - } else { - return EthTxArgs{}, - fmt.Errorf("invalid methodnum %d: only allowed method is InvokeContract(%d)", - msg.Method, builtintypes.MethodsEVM.InvokeContract) - } - - return EthTxArgs{ - ChainID: Eip155ChainID, - Nonce: int(msg.Nonce), - To: to, - Value: msg.Value, - Input: params, - MaxFeePerGas: msg.GasFeeCap, - MaxPriorityFeePerGas: msg.GasPremium, - GasLimit: int(msg.GasLimit), - }, nil -} + // Convert Filecoin address to Ethereum address. + _, err := EthAddressFromFilecoinAddress(smsg.Message.From) + if err != nil { + return nil, fmt.Errorf("sender was not an eth account") + } -func (tx *EthTxArgs) ToUnsignedMessage(from address.Address) (*Message, error) { - if tx.ChainID != Eip155ChainID { - return nil, fmt.Errorf("unsupported chain id: %d", tx.ChainID) + // Extract Ethereum parameters and recipient from the message. + params, to, err := getEthParamsAndRecipient(&smsg.Message) + if err != nil { + return nil, fmt.Errorf("failed to parse input params and recipient: %w", err) } - var err error - var params []byte - if len(tx.Input) > 0 { - buf := new(bytes.Buffer) - if err = cbg.WriteByteArray(buf, tx.Input); err != nil { - return nil, fmt.Errorf("failed to write input args: %w", err) - } - params = buf.Bytes() + // Check for supported message version. + if smsg.Message.Version != 0 { + return nil, fmt.Errorf("unsupported msg version: %d", smsg.Message.Version) } - var to address.Address - var method abi.MethodNum - // nil indicates the EAM, only CreateExternal is allowed - if tx.To == nil { - method = builtintypes.MethodsEAM.CreateExternal - to = builtintypes.EthereumAddressManagerActorAddr - } else { - method = builtintypes.MethodsEVM.InvokeContract - to, err = tx.To.ToFilecoinAddress() - if err != nil { - return nil, fmt.Errorf("failed to convert To into filecoin addr: %w", err) + // Determine the type of transaction based on the signature length + switch len(smsg.Signature.Data) { + case EthEIP1559TxSignatureLen: + tx := Eth1559TxArgs{ + ChainID: Eip155ChainID, + Nonce: int(smsg.Message.Nonce), + To: to, + Value: smsg.Message.Value, + Input: params, + MaxFeePerGas: smsg.Message.GasFeeCap, + MaxPriorityFeePerGas: smsg.Message.GasPremium, + GasLimit: int(smsg.Message.GasLimit), + } + if err := tx.InitialiseSignature(smsg.Signature); err != nil { + return nil, fmt.Errorf("failed to initialise signature: %w", err) + } + return &tx, nil + + case EthLegacyHomesteadTxSignatureLen, EthLegacy155TxSignatureLen0, EthLegacy155TxSignatureLen1: + legacyTx := &EthLegacyHomesteadTxArgs{ + Nonce: int(smsg.Message.Nonce), + To: to, + Value: smsg.Message.Value, + Input: params, + GasPrice: smsg.Message.GasFeeCap, + GasLimit: int(smsg.Message.GasLimit), + } + // Process based on the first byte of the signature + switch smsg.Signature.Data[0] { + case EthLegacyHomesteadTxSignaturePrefix: + if err := legacyTx.InitialiseSignature(smsg.Signature); err != nil { + return nil, fmt.Errorf("failed to initialise signature: %w", err) + } + return legacyTx, nil + case EthLegacy155TxSignaturePrefix: + tx := &EthLegacy155TxArgs{ + legacyTx: legacyTx, + } + if err := tx.InitialiseSignature(smsg.Signature); err != nil { + return nil, fmt.Errorf("failed to initialise signature: %w", err) + } + return tx, nil + default: + return nil, fmt.Errorf("unsupported legacy transaction; first byte of signature is %d", smsg.Signature.Data[0]) } - } - return &Message{ - Version: 0, - To: to, - From: from, - Nonce: uint64(tx.Nonce), - Value: tx.Value, - GasLimit: int64(tx.GasLimit), - GasFeeCap: tx.MaxFeePerGas, - GasPremium: tx.MaxPriorityFeePerGas, - Method: method, - Params: params, - }, nil + default: + return nil, fmt.Errorf("unsupported signature length") + } } -func (tx *EthTxArgs) ToSignedMessage() (*SignedMessage, error) { +func ToSignedFilecoinMessage(tx EthTransaction) (*SignedMessage, error) { from, err := tx.Sender() if err != nil { return nil, fmt.Errorf("failed to calculate sender: %w", err) } - unsignedMsg, err := tx.ToUnsignedMessage(from) + unsignedMsg, err := tx.ToUnsignedFilecoinMessage(from) if err != nil { return nil, fmt.Errorf("failed to convert to unsigned msg: %w", err) } @@ -210,443 +210,386 @@ func (tx *EthTxArgs) ToSignedMessage() (*SignedMessage, error) { }, nil } -func (tx *EthTxArgs) HashedOriginalRlpMsg() ([]byte, error) { - msg, err := tx.ToRlpUnsignedMsg() - if err != nil { - return nil, err - } - - hasher := sha3.NewLegacyKeccak256() - hasher.Write(msg) - hash := hasher.Sum(nil) - return hash, nil -} - -func (tx *EthTxArgs) ToRlpUnsignedMsg() ([]byte, error) { - packed, err := tx.packTxFields() - if err != nil { - return nil, err +func ParseEthTransaction(data []byte) (EthTransaction, error) { + if len(data) == 0 { + return nil, fmt.Errorf("empty data") } - encoded, err := EncodeRLP(packed) - if err != nil { - return nil, err + switch data[0] { + case 1: + // EIP-2930 + return nil, fmt.Errorf("EIP-2930 transaction is not supported") + case EIP1559TxType: + // EIP-1559 + return parseEip1559Tx(data) + default: + if data[0] > 0x7f { + tx, err := parseLegacyTx(data) + if err != nil { + return nil, fmt.Errorf("failed to parse legacy transaction: %w", err) + } + return tx, nil + } } - return append([]byte{0x02}, encoded...), nil -} -func (tx *EthTx) ToEthTxArgs() EthTxArgs { - return EthTxArgs{ - ChainID: int(tx.ChainID), - Nonce: int(tx.Nonce), - To: tx.To, - Value: big.Int(tx.Value), - MaxFeePerGas: big.Int(tx.MaxFeePerGas), - MaxPriorityFeePerGas: big.Int(tx.MaxPriorityFeePerGas), - GasLimit: int(tx.Gas), - Input: tx.Input, - V: big.Int(tx.V), - R: big.Int(tx.R), - S: big.Int(tx.S), - } + return nil, fmt.Errorf("unsupported transaction type") } -func (tx *EthTx) TxHash() (EthHash, error) { - ethTxArgs := tx.ToEthTxArgs() - return (ðTxArgs).TxHash() +type methodInfo struct { + to address.Address + method abi.MethodNum + params []byte } -func (tx *EthTxArgs) TxHash() (EthHash, error) { - rlp, err := tx.ToRlpSignedMsg() - if err != nil { - return EmptyEthHash, err +func getFilecoinMethodInfo(recipient *EthAddress, input []byte) (*methodInfo, error) { + var params []byte + if len(input) > 0 { + buf := new(bytes.Buffer) + if err := cbg.WriteByteArray(buf, input); err != nil { + return nil, fmt.Errorf("failed to write input args: %w", err) + } + params = buf.Bytes() } - return EthHashFromTxBytes(rlp), nil -} - -func (tx *EthTxArgs) ToRlpSignedMsg() ([]byte, error) { - packed1, err := tx.packTxFields() - if err != nil { - return nil, err - } + var to address.Address + var method abi.MethodNum - packed2, err := tx.packSigFields() - if err != nil { - return nil, err + if recipient == nil { + // If recipient is nil, use Ethereum Address Manager Actor and CreateExternal method + method = builtintypes.MethodsEAM.CreateExternal + to = builtintypes.EthereumAddressManagerActorAddr + } else { + // Otherwise, use InvokeContract method and convert EthAddress to Filecoin address + method = builtintypes.MethodsEVM.InvokeContract + var err error + to, err = recipient.ToFilecoinAddress() + if err != nil { + return nil, fmt.Errorf("failed to convert EthAddress to Filecoin address: %w", err) + } } - encoded, err := EncodeRLP(append(packed1, packed2...)) - if err != nil { - return nil, err - } - return append([]byte{0x02}, encoded...), nil + return &methodInfo{ + to: to, + method: method, + params: params, + }, nil } -func (tx *EthTxArgs) packTxFields() ([]interface{}, error) { - chainID, err := formatInt(tx.ChainID) - if err != nil { - return nil, err - } - - nonce, err := formatInt(tx.Nonce) +func packSigFields(v, r, s big.Int) ([]interface{}, error) { + rr, err := formatBigInt(r) if err != nil { return nil, err } - maxPriorityFeePerGas, err := formatBigInt(tx.MaxPriorityFeePerGas) + ss, err := formatBigInt(s) if err != nil { return nil, err } - maxFeePerGas, err := formatBigInt(tx.MaxFeePerGas) + vv, err := formatBigInt(v) if err != nil { return nil, err } - gasLimit, err := formatInt(tx.GasLimit) - if err != nil { - return nil, err - } + res := []interface{}{vv, rr, ss} + return res, nil +} - value, err := formatBigInt(tx.Value) - if err != nil { - return nil, err +func padLeadingZeros(data []byte, length int) []byte { + if len(data) >= length { + return data } + zeros := make([]byte, length-len(data)) + return append(zeros, data...) +} - res := []interface{}{ - chainID, - nonce, - maxPriorityFeePerGas, - maxFeePerGas, - gasLimit, - formatEthAddr(tx.To), - value, - tx.Input, - []interface{}{}, // access list +func removeLeadingZeros(data []byte) []byte { + firstNonZeroIndex := len(data) + for i, b := range data { + if b > 0 { + firstNonZeroIndex = i + break + } } - return res, nil + return data[firstNonZeroIndex:] } -func (tx *EthTxArgs) packSigFields() ([]interface{}, error) { - r, err := formatBigInt(tx.R) +func formatInt(val int) ([]byte, error) { + buf := new(bytes.Buffer) + err := binary.Write(buf, binary.BigEndian, int64(val)) if err != nil { return nil, err } + return removeLeadingZeros(buf.Bytes()), nil +} - s, err := formatBigInt(tx.S) - if err != nil { - return nil, err +func formatEthAddr(addr *EthAddress) []byte { + if addr == nil { + return nil } + return addr[:] +} - v, err := formatBigInt(tx.V) +func formatBigInt(val big.Int) ([]byte, error) { + b, err := val.Bytes() if err != nil { return nil, err } - - res := []interface{}{v, r, s} - return res, nil + return removeLeadingZeros(b), nil } -func (tx *EthTxArgs) Signature() (*typescrypto.Signature, error) { - r := tx.R.Int.Bytes() - s := tx.S.Int.Bytes() - v := tx.V.Int.Bytes() - - sig := append([]byte{}, padLeadingZeros(r, 32)...) - sig = append(sig, padLeadingZeros(s, 32)...) - if len(v) == 0 { - sig = append(sig, 0) - } else { - sig = append(sig, v[0]) +func parseInt(v interface{}) (int, error) { + data, ok := v.([]byte) + if !ok { + return 0, fmt.Errorf("cannot parse interface to int: input is not a byte array") } - - if len(sig) != 65 { - return nil, fmt.Errorf("signature is not 65 bytes") + if len(data) == 0 { + return 0, nil } - return &typescrypto.Signature{ - Type: typescrypto.SigTypeDelegated, Data: sig, - }, nil -} - -func (tx *EthTxArgs) Sender() (address.Address, error) { - msg, err := tx.ToRlpUnsignedMsg() - if err != nil { - return address.Undef, err + if len(data) > 8 { + return 0, fmt.Errorf("cannot parse interface to int: length is more than 8 bytes") } - - hasher := sha3.NewLegacyKeccak256() - hasher.Write(msg) - hash := hasher.Sum(nil) - - sig, err := tx.Signature() - if err != nil { - return address.Undef, err + var value int64 + r := bytes.NewReader(append(make([]byte, 8-len(data)), data...)) + if err := binary.Read(r, binary.BigEndian, &value); err != nil { + return 0, fmt.Errorf("cannot parse interface to EthUint64: %w", err) } + return int(value), nil +} - pubk, err := gocrypto.EcRecover(hash, sig.Data) - if err != nil { - return address.Undef, err +func parseBigInt(v interface{}) (big.Int, error) { + data, ok := v.([]byte) + if !ok { + return big.Zero(), fmt.Errorf("cannot parse interface to big.Int: input is not a byte array") } - - ethAddr, err := EthAddressFromPubKey(pubk) - if err != nil { - return address.Undef, err + if len(data) == 0 { + return big.Zero(), nil } + var b mathbig.Int + b.SetBytes(data) + return big.NewFromGo(&b), nil +} - ea, err := CastEthAddress(ethAddr) - if err != nil { - return address.Undef, err +func parseBytes(v interface{}) ([]byte, error) { + val, ok := v.([]byte) + if !ok { + return nil, fmt.Errorf("cannot parse interface into bytes: input is not a byte array") } - - return ea.ToFilecoinAddress() + return val, nil } -func RecoverSignature(sig typescrypto.Signature) (EthBigInt, EthBigInt, EthBigInt, error) { - if sig.Type != typescrypto.SigTypeDelegated { - return EthBigIntZero, EthBigIntZero, EthBigIntZero, fmt.Errorf("RecoverSignature only supports Delegated signature") +func parseEthAddr(v interface{}) (*EthAddress, error) { + b, err := parseBytes(v) + if err != nil { + return nil, err } - - if len(sig.Data) != 65 { - return EthBigIntZero, EthBigIntZero, EthBigIntZero, fmt.Errorf("signature should be 65 bytes long, but got %d bytes", len(sig.Data)) + if len(b) == 0 { + return nil, nil } - - r, err := parseBigInt(sig.Data[0:32]) + addr, err := CastEthAddress(b) if err != nil { - return EthBigIntZero, EthBigIntZero, EthBigIntZero, fmt.Errorf("cannot parse r into EthBigInt") + return nil, err } + return &addr, nil +} - s, err := parseBigInt(sig.Data[32:64]) - if err != nil { - return EthBigIntZero, EthBigIntZero, EthBigIntZero, fmt.Errorf("cannot parse s into EthBigInt") +func getEthParamsAndRecipient(msg *Message) (params []byte, to *EthAddress, err error) { + if len(msg.Params) > 0 { + paramsReader := bytes.NewReader(msg.Params) + var err error + params, err = cbg.ReadByteArray(paramsReader, uint64(len(msg.Params))) + if err != nil { + return nil, nil, fmt.Errorf("failed to read params byte array: %w", err) + } + if paramsReader.Len() != 0 { + return nil, nil, fmt.Errorf("extra data found in params") + } + if len(params) == 0 { + return nil, nil, fmt.Errorf("non-empty params encode empty byte array") + } } - v, err := parseBigInt([]byte{sig.Data[64]}) - if err != nil { - return EthBigIntZero, EthBigIntZero, EthBigIntZero, fmt.Errorf("cannot parse v into EthBigInt") + if msg.To == builtintypes.EthereumAddressManagerActorAddr { + if msg.Method != builtintypes.MethodsEAM.CreateExternal { + return nil, nil, fmt.Errorf("unsupported EAM method") + } + } else if msg.Method == builtintypes.MethodsEVM.InvokeContract { + addr, err := EthAddressFromFilecoinAddress(msg.To) + if err != nil { + return nil, nil, err + } + to = &addr + } else { + return nil, nil, + fmt.Errorf("invalid methodnum %d: only allowed method is InvokeContract(%d) or CreateExternal(%d)", + msg.Method, builtintypes.MethodsEVM.InvokeContract, builtintypes.MethodsEAM.CreateExternal) } - return EthBigInt(r), EthBigInt(s), EthBigInt(v), nil + return params, to, nil } -func parseEip1559Tx(data []byte) (*EthTxArgs, error) { - if data[0] != 2 { - return nil, fmt.Errorf("not an EIP-1559 transaction: first byte is not 2") +func parseLegacyTx(data []byte) (EthTransaction, error) { + if data[0] <= 0x7f { + return nil, fmt.Errorf("not a legacy eth transaction") } - d, err := DecodeRLP(data[1:]) + d, err := DecodeRLP(data) if err != nil { return nil, err } decoded, ok := d.([]interface{}) if !ok { - return nil, fmt.Errorf("not an EIP-1559 transaction: decoded data is not a list") - } - - if len(decoded) != 12 { - return nil, fmt.Errorf("not an EIP-1559 transaction: should have 12 elements in the rlp list") - } - - chainID, err := parseInt(decoded[0]) - if err != nil { - return nil, err - } - - nonce, err := parseInt(decoded[1]) - if err != nil { - return nil, err + return nil, fmt.Errorf("not a Legacy transaction: decoded data is not a list") } - maxPriorityFeePerGas, err := parseBigInt(decoded[2]) - if err != nil { - return nil, err + if len(decoded) != 9 { + return nil, fmt.Errorf("not a Legacy transaction: should have 9 elements in the rlp list") } - maxFeePerGas, err := parseBigInt(decoded[3]) + nonce, err := parseInt(decoded[0]) if err != nil { return nil, err } - gasLimit, err := parseInt(decoded[4]) + gasPrice, err := parseBigInt(decoded[1]) if err != nil { return nil, err } - to, err := parseEthAddr(decoded[5]) + gasLimit, err := parseInt(decoded[2]) if err != nil { return nil, err } - value, err := parseBigInt(decoded[6]) + to, err := parseEthAddr(decoded[3]) if err != nil { return nil, err } - input, err := parseBytes(decoded[7]) + value, err := parseBigInt(decoded[4]) if err != nil { return nil, err } - accessList, ok := decoded[8].([]interface{}) - if !ok || (ok && len(accessList) != 0) { - return nil, fmt.Errorf("access list should be an empty list") + input, ok := decoded[5].([]byte) + if !ok { + return nil, fmt.Errorf("input is not a byte slice") } - r, err := parseBigInt(decoded[10]) + v, err := parseBigInt(decoded[6]) if err != nil { return nil, err } - s, err := parseBigInt(decoded[11]) + r, err := parseBigInt(decoded[7]) if err != nil { return nil, err } - v, err := parseBigInt(decoded[9]) + s, err := parseBigInt(decoded[8]) if err != nil { return nil, err } - // EIP-1559 and EIP-2930 transactions only support 0 or 1 for v - // Legacy and EIP-155 transactions support other values - // https://github.com/ethers-io/ethers.js/blob/56fabe987bb8c1e4891fdf1e5d3fe8a4c0471751/packages/transactions/src.ts/index.ts#L333 - if !v.Equals(big.NewInt(0)) && !v.Equals(big.NewInt(1)) { - return nil, fmt.Errorf("EIP-1559 transactions only support 0 or 1 for v") + tx := &EthLegacyHomesteadTxArgs{ + Nonce: nonce, + GasPrice: gasPrice, + GasLimit: gasLimit, + To: to, + Value: value, + Input: input, + V: v, + R: r, + S: s, } - args := EthTxArgs{ - ChainID: chainID, - Nonce: nonce, - To: to, - MaxPriorityFeePerGas: maxPriorityFeePerGas, - MaxFeePerGas: maxFeePerGas, - GasLimit: gasLimit, - Value: value, - Input: input, - R: r, - S: s, - V: v, - } - return &args, nil -} - -func ParseEthTxArgs(data []byte) (*EthTxArgs, error) { - if len(data) == 0 { - return nil, fmt.Errorf("empty data") - } - - if data[0] > 0x7f { - // legacy transaction - return nil, fmt.Errorf("legacy transaction is not supported") + chainId := deriveEIP155ChainId(v) + if chainId.Equals(big.NewInt(0)) { + // This is a legacy Homestead transaction + if !v.Equals(big.NewInt(27)) && !v.Equals(big.NewInt(28)) { + return nil, fmt.Errorf("legacy homestead transactions only support 27 or 28 for v, got %d", v.Uint64()) + } + return tx, nil } - if data[0] == 1 { - // EIP-2930 - return nil, fmt.Errorf("EIP-2930 transaction is not supported") + // This is a EIP-155 transaction -> ensure chainID protection + if err := validateEIP155ChainId(v); err != nil { + return nil, fmt.Errorf("failed to validate EIP155 chain id: %w", err) } - if data[0] == Eip1559TxType { - // EIP-1559 - return parseEip1559Tx(data) - } - - return nil, fmt.Errorf("unsupported transaction type") + return &EthLegacy155TxArgs{ + legacyTx: tx, + }, nil } -func padLeadingZeros(data []byte, length int) []byte { - if len(data) >= length { - return data - } - zeros := make([]byte, length-len(data)) - return append(zeros, data...) +type RlpPackable interface { + packTxFields() ([]interface{}, error) } -func removeLeadingZeros(data []byte) []byte { - firstNonZeroIndex := len(data) - for i, b := range data { - if b > 0 { - firstNonZeroIndex = i - break - } +func toRlpUnsignedMsg(tx RlpPackable) ([]byte, error) { + packedFields, err := tx.packTxFields() + if err != nil { + return nil, err } - return data[firstNonZeroIndex:] -} - -func formatInt(val int) ([]byte, error) { - buf := new(bytes.Buffer) - err := binary.Write(buf, binary.BigEndian, int64(val)) + encoded, err := EncodeRLP(packedFields) if err != nil { return nil, err } - return removeLeadingZeros(buf.Bytes()), nil + return encoded, nil } -func formatEthAddr(addr *EthAddress) []byte { - if addr == nil { - return nil +func toRlpSignedMsg(tx RlpPackable, V, R, S big.Int) ([]byte, error) { + packed1, err := tx.packTxFields() + if err != nil { + return nil, err } - return addr[:] -} -func formatBigInt(val big.Int) ([]byte, error) { - b, err := val.Bytes() + packed2, err := packSigFields(V, R, S) if err != nil { return nil, err } - return removeLeadingZeros(b), nil -} -func parseInt(v interface{}) (int, error) { - data, ok := v.([]byte) - if !ok { - return 0, fmt.Errorf("cannot parse interface to int: input is not a byte array") - } - if len(data) == 0 { - return 0, nil - } - if len(data) > 8 { - return 0, fmt.Errorf("cannot parse interface to int: length is more than 8 bytes") - } - var value int64 - r := bytes.NewReader(append(make([]byte, 8-len(data)), data...)) - if err := binary.Read(r, binary.BigEndian, &value); err != nil { - return 0, fmt.Errorf("cannot parse interface to EthUint64: %w", err) + encoded, err := EncodeRLP(append(packed1, packed2...)) + if err != nil { + return nil, fmt.Errorf("failed to encode rlp signed msg: %w", err) } - return int(value), nil + return encoded, nil } -func parseBigInt(v interface{}) (big.Int, error) { - data, ok := v.([]byte) - if !ok { - return big.Zero(), fmt.Errorf("cannot parse interface to big.Int: input is not a byte array") +func sender(tx EthTransaction) (address.Address, error) { + msg, err := tx.ToRlpUnsignedMsg() + if err != nil { + return address.Undef, fmt.Errorf("failed to get rlp unsigned msg: %w", err) } - if len(data) == 0 { - return big.Zero(), nil + + hasher := sha3.NewLegacyKeccak256() + hasher.Write(msg) + hash := hasher.Sum(nil) + + sig, err := tx.Signature() + if err != nil { + return address.Undef, fmt.Errorf("failed to get signature: %w", err) } - var b mathbig.Int - b.SetBytes(data) - return big.NewFromGo(&b), nil -} -func parseBytes(v interface{}) ([]byte, error) { - val, ok := v.([]byte) - if !ok { - return nil, fmt.Errorf("cannot parse interface into bytes: input is not a byte array") + sigData, err := tx.ToVerifiableSignature(sig.Data) + if err != nil { + return address.Undef, fmt.Errorf("failed to get verifiable signature: %w", err) } - return val, nil -} -func parseEthAddr(v interface{}) (*EthAddress, error) { - b, err := parseBytes(v) + pubk, err := gocrypto.EcRecover(hash, sigData) if err != nil { - return nil, err + return address.Undef, fmt.Errorf("failed to recover pubkey: %w", err) } - if len(b) == 0 { - return nil, nil + + ethAddr, err := EthAddressFromPubKey(pubk) + if err != nil { + return address.Undef, fmt.Errorf("failed to get eth address from pubkey: %w", err) } - addr, err := CastEthAddress(b) + + ea, err := CastEthAddress(ethAddr) if err != nil { - return nil, err + return address.Undef, fmt.Errorf("failed to cast eth address: %w", err) } - return &addr, nil + + return ea.ToFilecoinAddress() } diff --git a/venus-shared/actors/types/eth_transactions_test.go b/venus-shared/actors/types/eth_transactions_test.go new file mode 100644 index 0000000000..63f574d153 --- /dev/null +++ b/venus-shared/actors/types/eth_transactions_test.go @@ -0,0 +1,165 @@ +package types + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/filecoin-project/go-address" + "github.com/filecoin-project/go-state-types/big" + builtintypes "github.com/filecoin-project/go-state-types/builtin" + "github.com/filecoin-project/go-state-types/crypto" +) + +func TestEthTransactionFromSignedFilecoinMessage(t *testing.T) { + eip1559sig := make([]byte, 65) + eip1559sig[0] = 1 + + legacySig := make([]byte, 66) + legacySig[0] = 1 + legacySig[65] = 27 + + pubKeyHex := "0x04cfecc0520d906cbfea387759246e89d85e2998843e56ad1c41de247ce10b3e4c453aa73c8de13c178d94461b6fa3f8b6f74406ce43d2fbab6992d0b283394242" + pubk := mustDecodeHex(pubKeyHex) + addrHash, err := EthAddressFromPubKey(pubk) + require.NoError(t, err) + from, err := address.NewDelegatedAddress(builtintypes.EthereumAddressManagerActorID, addrHash) + require.NoError(t, err) + + fromEth, err := EthAddressFromFilecoinAddress(from) + require.NoError(t, err) + + to, err := address.NewIDAddress(1) + require.NoError(t, err) + + toEth, err := EthAddressFromFilecoinAddress(to) + require.NoError(t, err) + + tcs := map[string]struct { + msg *SignedMessage + expectedErr string + validateFunc func(t *testing.T, smsg *SignedMessage, tx EthTransaction) + }{ + "empty": { + expectedErr: "signed message is nil", + }, + "invalid-signature": { + msg: &SignedMessage{ + Message: Message{ + To: builtintypes.EthereumAddressManagerActorAddr, + From: from, + Method: builtintypes.MethodsEAM.CreateExternal, + }, + Signature: crypto.Signature{ + Type: crypto.SigTypeDelegated, + Data: []byte{1}, + }, + }, + expectedErr: "unsupported signature length", + }, + "valid-eip1559": { + msg: &SignedMessage{ + Message: Message{ + From: from, + To: to, + Value: big.NewInt(10), + GasFeeCap: big.NewInt(11), + GasPremium: big.NewInt(12), + GasLimit: 13, + Nonce: 14, + Method: builtintypes.MethodsEVM.InvokeContract, + }, + Signature: crypto.Signature{ + Type: crypto.SigTypeDelegated, + Data: eip1559sig, + }, + }, + validateFunc: func(t *testing.T, smsg *SignedMessage, tx EthTransaction) { + eip1559tx := tx.(*Eth1559TxArgs) + require.Equal(t, big.NewInt(10), eip1559tx.Value) + require.Equal(t, big.NewInt(11), eip1559tx.MaxFeePerGas) + require.Equal(t, big.NewInt(12), eip1559tx.MaxPriorityFeePerGas) + require.EqualValues(t, uint64(13), eip1559tx.GasLimit) + require.EqualValues(t, uint64(14), eip1559tx.Nonce) + require.EqualValues(t, toEth, *eip1559tx.To) + require.EqualValues(t, 314, eip1559tx.ChainID) + require.Empty(t, eip1559tx.Input) + + ethTx, err := tx.ToEthTx(smsg) + require.NoError(t, err) + require.EqualValues(t, 314, ethTx.ChainID) + require.EqualValues(t, 14, ethTx.Nonce) + hash, err := eip1559tx.TxHash() + require.NoError(t, err) + require.EqualValues(t, hash, ethTx.Hash) + require.EqualValues(t, fromEth, ethTx.From) + require.EqualValues(t, toEth, *ethTx.To) + require.EqualValues(t, big.NewInt(10), ethTx.Value) + require.EqualValues(t, 13, ethTx.Gas) + require.EqualValues(t, big.NewInt(11), *ethTx.MaxFeePerGas) + require.EqualValues(t, big.NewInt(12), *ethTx.MaxPriorityFeePerGas) + require.Nil(t, ethTx.GasPrice) + require.Empty(t, ethTx.AccessList) + }, + }, + "valid-legacy": { + msg: &SignedMessage{ + Message: Message{ + From: from, + To: to, + Value: big.NewInt(10), + GasFeeCap: big.NewInt(11), + GasPremium: big.NewInt(12), + GasLimit: 13, + Nonce: 14, + Method: builtintypes.MethodsEVM.InvokeContract, + }, + Signature: crypto.Signature{ + Type: crypto.SigTypeDelegated, + Data: legacySig, + }, + }, + validateFunc: func(t *testing.T, smsg *SignedMessage, tx EthTransaction) { + legacyTx := tx.(*EthLegacyHomesteadTxArgs) + require.Equal(t, big.NewInt(10), legacyTx.Value) + require.EqualValues(t, uint64(13), legacyTx.GasLimit) + require.EqualValues(t, uint64(14), legacyTx.Nonce) + require.EqualValues(t, toEth, *legacyTx.To) + require.EqualValues(t, big.NewInt(11), legacyTx.GasPrice) + require.Empty(t, legacyTx.Input) + + ethTx, err := tx.ToEthTx(smsg) + require.NoError(t, err) + require.EqualValues(t, 0, ethTx.ChainID) + require.EqualValues(t, 14, ethTx.Nonce) + hash, err := legacyTx.TxHash() + require.NoError(t, err) + require.EqualValues(t, big.NewInt(11), *ethTx.GasPrice) + require.EqualValues(t, hash, ethTx.Hash) + require.EqualValues(t, fromEth, ethTx.From) + require.EqualValues(t, toEth, *ethTx.To) + require.EqualValues(t, big.NewInt(10), ethTx.Value) + require.EqualValues(t, 13, ethTx.Gas) + require.Nil(t, ethTx.MaxFeePerGas) + require.Nil(t, ethTx.MaxPriorityFeePerGas) + require.Empty(t, ethTx.AccessList) + require.EqualValues(t, big.NewInt(27), ethTx.V) + }, + }, + } + + for name, tc := range tcs { + t.Run(name, func(t *testing.T) { + tx, err := EthTransactionFromSignedFilecoinMessage(tc.msg) + if tc.expectedErr != "" { + require.Error(t, err) + require.Contains(t, err.Error(), tc.expectedErr) + } else { + require.NoError(t, err) + } + if tc.validateFunc != nil { + tc.validateFunc(t, tc.msg, tx) + } + }) + } +} diff --git a/venus-shared/actors/types/message.go b/venus-shared/actors/types/message.go index e7535bda1f..4b30b2af68 100644 --- a/venus-shared/actors/types/message.go +++ b/venus-shared/actors/types/message.go @@ -241,7 +241,7 @@ func (m *Message) RequiredFunds() abi.TokenAmount { func (m *Message) SigningBytes(sigType crypto.SigType) ([]byte, error) { if sigType == crypto.SigTypeDelegated { - txArgs, err := EthTxArgsFromUnsignedEthMessage(m) + txArgs, err := Eth1559TxArgsFromUnsignedFilecoinMessage(m) if err != nil { return nil, fmt.Errorf("failed to reconstruct eth transaction: %w", err) } diff --git a/venus-shared/actors/types/rlp_test.go b/venus-shared/actors/types/rlp_test.go index 7396dee83e..3492719a46 100644 --- a/venus-shared/actors/types/rlp_test.go +++ b/venus-shared/actors/types/rlp_test.go @@ -11,11 +11,6 @@ import ( "github.com/filecoin-project/go-address" ) -type TestCase struct { - Input interface{} - Output interface{} -} - func TestEncode(t *testing.T) { testcases := []TestCase{ {[]byte(""), mustDecodeHex("0x80")}, @@ -197,7 +192,7 @@ func TestDecodeError(t *testing.T) { func TestDecode1(t *testing.T) { b := mustDecodeHex("0x02f8758401df5e7680832c8411832c8411830767f89452963ef50e27e06d72d59fcb4f3c2a687be3cfef880de0b6b3a764000080c080a094b11866f453ad85a980e0e8a2fc98cbaeb4409618c7734a7e12ae2f66fd405da042dbfb1b37af102023830ceeee0e703ffba0b8b3afeb8fe59f405eca9ed61072") - decoded, err := ParseEthTxArgs(b) + decoded, err := parseEip1559Tx(b) require.NoError(t, err) sender, err := decoded.Sender() diff --git a/venus-shared/types/eth_1559_transactions.go b/venus-shared/types/eth_1559_transactions.go new file mode 100755 index 0000000000..4deb19642e --- /dev/null +++ b/venus-shared/types/eth_1559_transactions.go @@ -0,0 +1,14 @@ +// Code generated by github.com/filecoin-project/venus/venus-devtool/state-type-gen. DO NOT EDIT. +package types + +import ( + "github.com/filecoin-project/venus/venus-shared/actors/types" +) + +type ( + Eth1559TxArgs = types.Eth1559TxArgs +) + +var ( + Eth1559TxArgsFromUnsignedFilecoinMessage = types.Eth1559TxArgsFromUnsignedFilecoinMessage +) diff --git a/venus-shared/types/eth_legacy_155_transactions.go b/venus-shared/types/eth_legacy_155_transactions.go new file mode 100755 index 0000000000..1b239e1b0c --- /dev/null +++ b/venus-shared/types/eth_legacy_155_transactions.go @@ -0,0 +1,14 @@ +// Code generated by github.com/filecoin-project/venus/venus-devtool/state-type-gen. DO NOT EDIT. +package types + +import ( + "github.com/filecoin-project/venus/venus-shared/actors/types" +) + +type ( + EthLegacy155TxArgs = types.EthLegacy155TxArgs +) + +var ( + NewEthLegacy155TxArgs = types.NewEthLegacy155TxArgs +) diff --git a/venus-shared/types/eth_legacy_homestead_transactions.go b/venus-shared/types/eth_legacy_homestead_transactions.go new file mode 100755 index 0000000000..3fec0ceb1f --- /dev/null +++ b/venus-shared/types/eth_legacy_homestead_transactions.go @@ -0,0 +1,10 @@ +// Code generated by github.com/filecoin-project/venus/venus-devtool/state-type-gen. DO NOT EDIT. +package types + +import ( + "github.com/filecoin-project/venus/venus-shared/actors/types" +) + +type ( + EthLegacyHomesteadTxArgs = types.EthLegacyHomesteadTxArgs +) diff --git a/venus-shared/types/eth_transactions.go b/venus-shared/types/eth_transactions.go index 098865974f..40eb42fd86 100644 --- a/venus-shared/types/eth_transactions.go +++ b/venus-shared/types/eth_transactions.go @@ -6,17 +6,30 @@ import ( ) const ( - Eip1559TxType = types.Eip1559TxType + EIP1559TxType = types.EIP1559TxType + EthEIP1559TxSignatureLen = types.EthEIP1559TxSignatureLen + EthLegacy155TxSignaturePrefix = types.EthLegacy155TxSignaturePrefix + EthLegacyHomesteadTxChainID = types.EthLegacyHomesteadTxChainID + EthLegacyHomesteadTxSignatureLen = types.EthLegacyHomesteadTxSignatureLen + EthLegacyHomesteadTxSignaturePrefix = types.EthLegacyHomesteadTxSignaturePrefix + EthLegacyTxType = types.EthLegacyTxType ) +var ( + EthLegacy155TxSignatureLen0 = types.EthLegacy155TxSignatureLen0 + EthLegacy155TxSignatureLen1 = types.EthLegacy155TxSignatureLen1 +) + +type ( + EthTransaction = types.EthTransaction + RlpPackable = types.RlpPackable +) type ( - EthTx = types.EthTx - EthTxArgs = types.EthTxArgs + EthTx = types.EthTx ) var ( - EthTxArgsFromUnsignedEthMessage = types.EthTxArgsFromUnsignedEthMessage - EthTxFromSignedEthMessage = types.EthTxFromSignedEthMessage - ParseEthTxArgs = types.ParseEthTxArgs - RecoverSignature = types.RecoverSignature + EthTransactionFromSignedFilecoinMessage = types.EthTransactionFromSignedFilecoinMessage + ParseEthTransaction = types.ParseEthTransaction + ToSignedFilecoinMessage = types.ToSignedFilecoinMessage ) diff --git a/venus-shared/types/rlp_test.go b/venus-shared/types/rlp_test.go deleted file mode 100644 index e2da12e7ed..0000000000 --- a/venus-shared/types/rlp_test.go +++ /dev/null @@ -1,190 +0,0 @@ -package types - -import ( - "encoding/hex" - "fmt" - "strings" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/filecoin-project/go-address" -) - -func TestEncode(t *testing.T) { - testcases := []TestCase{ - {[]byte(""), mustDecodeHex("0x80")}, - {mustDecodeHex("0x01"), mustDecodeHex("0x01")}, - {mustDecodeHex("0xaa"), mustDecodeHex("0x81aa")}, - {mustDecodeHex("0x0402"), mustDecodeHex("0x820402")}, - { - []interface{}{}, - mustDecodeHex("0xc0"), - }, - { - mustDecodeHex("0xabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"), - mustDecodeHex("0xb83cabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"), - }, - { - mustDecodeHex("0xabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"), - mustDecodeHex("0xb8aaabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"), - }, - { - []interface{}{ - mustDecodeHex("0xaaaa"), - mustDecodeHex("0xbbbb"), - mustDecodeHex("0xcccc"), - mustDecodeHex("0xdddd"), - }, - mustDecodeHex("0xcc82aaaa82bbbb82cccc82dddd"), - }, - { - []interface{}{ - mustDecodeHex("0xaaaaaaaaaaaaaaaaaaaa"), - mustDecodeHex("0xbbbbbbbbbbbbbbbbbbbb"), - []interface{}{ - mustDecodeHex("0xc1c1c1c1c1c1c1c1c1c1"), - mustDecodeHex("0xc2c2c2c2c2c2c2c2c2c2"), - mustDecodeHex("0xc3c3c3c3c3c3c3c3c3c3"), - }, - mustDecodeHex("0xdddddddddddddddddddd"), - mustDecodeHex("0xeeeeeeeeeeeeeeeeeeee"), - mustDecodeHex("0xffffffffffffffffffff"), - }, - mustDecodeHex("0xf8598aaaaaaaaaaaaaaaaaaaaa8abbbbbbbbbbbbbbbbbbbbe18ac1c1c1c1c1c1c1c1c1c18ac2c2c2c2c2c2c2c2c2c28ac3c3c3c3c3c3c3c3c3c38adddddddddddddddddddd8aeeeeeeeeeeeeeeeeeeee8affffffffffffffffffff"), - }, - } - - for _, tc := range testcases { - result, err := EncodeRLP(tc.Input) - require.Nil(t, err) - - require.Equal(t, tc.Output.([]byte), result) - } -} - -func TestDecodeString(t *testing.T) { - testcases := []TestCase{ - {"0x00", "0x00"}, - {"0x80", "0x"}, - {"0x0f", "0x0f"}, - {"0x81aa", "0xaa"}, - {"0x820400", "0x0400"}, - {"0xb83cabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd", - "0xabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd"}, - } - - for _, tc := range testcases { - input, err := hex.DecodeString(strings.Replace(tc.Input.(string), "0x", "", -1)) - require.Nil(t, err) - - output, err := hex.DecodeString(strings.Replace(tc.Output.(string), "0x", "", -1)) - require.Nil(t, err) - - result, err := DecodeRLP(input) - require.Nil(t, err) - require.Equal(t, output, result.([]byte)) - } -} - -func mustDecodeHex(s string) []byte { - d, err := hex.DecodeString(strings.Replace(s, "0x", "", -1)) - if err != nil { - panic(fmt.Errorf("err must be nil: %w", err)) - } - return d -} - -func TestDecodeList(t *testing.T) { - testcases := []TestCase{ - {"0xc0", []interface{}{}}, - {"0xc100", []interface{}{[]byte{0}}}, - {"0xc3000102", []interface{}{[]byte{0}, []byte{1}, []byte{2}}}, - {"0xc4000181aa", []interface{}{[]byte{0}, []byte{1}, []byte{0xaa}}}, - {"0xc6000181aa81ff", []interface{}{[]byte{0}, []byte{1}, []byte{0xaa}, []byte{0xff}}}, - {"0xf8428aabcdabcdabcdabcdabcd8aabcdabcdabcdabcdabcd8aabcdabcdabcdabcdabcd8aabcdabcdabcdabcdabcd8aabcdabcdabcdabcdabcd8aabcdabcdabcdabcdabcd", - []interface{}{ - mustDecodeHex("0xabcdabcdabcdabcdabcd"), - mustDecodeHex("0xabcdabcdabcdabcdabcd"), - mustDecodeHex("0xabcdabcdabcdabcdabcd"), - mustDecodeHex("0xabcdabcdabcdabcdabcd"), - mustDecodeHex("0xabcdabcdabcdabcdabcd"), - mustDecodeHex("0xabcdabcdabcdabcdabcd"), - }, - }, - {"0xf1030185012a05f2008504a817c800825208942b87d1cb599bc2a606db9a0169fcec96af04ad3a880de0b6b3a764000080c0", - []interface{}{ - []byte{3}, - []byte{1}, - mustDecodeHex("0x012a05f200"), - mustDecodeHex("0x04a817c800"), - mustDecodeHex("0x5208"), - mustDecodeHex("0x2b87d1CB599Bc2a606Db9A0169fcEc96Af04ad3a"), - mustDecodeHex("0x0de0b6b3a7640000"), - []byte{}, - []interface{}{}, - }}, - } - - for _, tc := range testcases { - input, err := hex.DecodeString(strings.Replace(tc.Input.(string), "0x", "", -1)) - require.Nil(t, err) - - result, err := DecodeRLP(input) - require.Nil(t, err) - - fmt.Println(result) - r := result.([]interface{}) - require.Equal(t, len(tc.Output.([]interface{})), len(r)) - - for i, v := range r { - require.Equal(t, tc.Output.([]interface{})[i], v) - } - } -} - -func TestDecodeEncodeTx(t *testing.T) { - testcases := [][]byte{ - mustDecodeHex("0xdc82013a0185012a05f2008504a817c8008080872386f26fc1000000c0"), - mustDecodeHex("0xf85f82013a0185012a05f2008504a817c8008080872386f26fc1000000c001a027fa36fb9623e4d71fcdd7f7dce71eb814c9560dcf3908c1719386e2efd122fba05fb4e4227174eeb0ba84747a4fb883c8d4e0fdb129c4b1f42e90282c41480234"), - mustDecodeHex("0xf9061c82013a0185012a05f2008504a817c8008080872386f26fc10000b905bb608060405234801561001057600080fd5b506127106000803273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002081905550610556806100656000396000f3fe608060405234801561001057600080fd5b50600436106100415760003560e01c80637bd703e81461004657806390b98a1114610076578063f8b2cb4f146100a6575b600080fd5b610060600480360381019061005b919061030a565b6100d6565b60405161006d9190610350565b60405180910390f35b610090600480360381019061008b9190610397565b6100f4565b60405161009d91906103f2565b60405180910390f35b6100c060048036038101906100bb919061030a565b61025f565b6040516100cd9190610350565b60405180910390f35b600060026100e38361025f565b6100ed919061043c565b9050919050565b6000816000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205410156101455760009050610259565b816000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008282546101939190610496565b92505081905550816000808573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008282546101e891906104ca565b925050819055508273ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef8460405161024c9190610350565b60405180910390a3600190505b92915050565b60008060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020549050919050565b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006102d7826102ac565b9050919050565b6102e7816102cc565b81146102f257600080fd5b50565b600081359050610304816102de565b92915050565b6000602082840312156103205761031f6102a7565b5b600061032e848285016102f5565b91505092915050565b6000819050919050565b61034a81610337565b82525050565b60006020820190506103656000830184610341565b92915050565b61037481610337565b811461037f57600080fd5b50565b6000813590506103918161036b565b92915050565b600080604083850312156103ae576103ad6102a7565b5b60006103bc858286016102f5565b92505060206103cd85828601610382565b9150509250929050565b60008115159050919050565b6103ec816103d7565b82525050565b600060208201905061040760008301846103e3565b92915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b600061044782610337565b915061045283610337565b9250817fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff048311821515161561048b5761048a61040d565b5b828202905092915050565b60006104a182610337565b91506104ac83610337565b9250828210156104bf576104be61040d565b5b828203905092915050565b60006104d582610337565b91506104e083610337565b9250827fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff038211156105155761051461040d565b5b82820190509291505056fea26469706673582212208e5b4b874c839967f88008ed2fa42d6c2d9c9b0ae05d1d2c61faa7d229c134e664736f6c634300080d0033c080a0c4e9477f57c6848b2f1ea73a14809c1f44529d20763c947f3ac8ffd3d1629d93a011485a215457579bb13ac7b53bb9d6804763ae6fe5ce8ddd41642cea55c9a09a"), - mustDecodeHex("0xf9063082013a0185012a05f2008504a817c8008094025b594a4f1c4888cafcfaf2bb24ed95507749e0872386f26fc10000b905bb608060405234801561001057600080fd5b506127106000803273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002081905550610556806100656000396000f3fe608060405234801561001057600080fd5b50600436106100415760003560e01c80637bd703e81461004657806390b98a1114610076578063f8b2cb4f146100a6575b600080fd5b610060600480360381019061005b919061030a565b6100d6565b60405161006d9190610350565b60405180910390f35b610090600480360381019061008b9190610397565b6100f4565b60405161009d91906103f2565b60405180910390f35b6100c060048036038101906100bb919061030a565b61025f565b6040516100cd9190610350565b60405180910390f35b600060026100e38361025f565b6100ed919061043c565b9050919050565b6000816000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205410156101455760009050610259565b816000803373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008282546101939190610496565b92505081905550816000808573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008282546101e891906104ca565b925050819055508273ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef8460405161024c9190610350565b60405180910390a3600190505b92915050565b60008060008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020549050919050565b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b60006102d7826102ac565b9050919050565b6102e7816102cc565b81146102f257600080fd5b50565b600081359050610304816102de565b92915050565b6000602082840312156103205761031f6102a7565b5b600061032e848285016102f5565b91505092915050565b6000819050919050565b61034a81610337565b82525050565b60006020820190506103656000830184610341565b92915050565b61037481610337565b811461037f57600080fd5b50565b6000813590506103918161036b565b92915050565b600080604083850312156103ae576103ad6102a7565b5b60006103bc858286016102f5565b92505060206103cd85828601610382565b9150509250929050565b60008115159050919050565b6103ec816103d7565b82525050565b600060208201905061040760008301846103e3565b92915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b600061044782610337565b915061045283610337565b9250817fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff048311821515161561048b5761048a61040d565b5b828202905092915050565b60006104a182610337565b91506104ac83610337565b9250828210156104bf576104be61040d565b5b828203905092915050565b60006104d582610337565b91506104e083610337565b9250827fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff038211156105155761051461040d565b5b82820190509291505056fea26469706673582212208e5b4b874c839967f88008ed2fa42d6c2d9c9b0ae05d1d2c61faa7d229c134e664736f6c634300080d0033c080a0fe38720928596f9e9dfbf891d00311638efce3713f03cdd67b212ecbbcf18f29a05993e656c0b35b8a580da6aff7c89b3d3e8b1c6f83a7ce09473c0699a8500b9c"), - } - - for _, tc := range testcases { - decoded, err := DecodeRLP(tc) - require.Nil(t, err) - - encoded, err := EncodeRLP(decoded) - require.Nil(t, err) - require.Equal(t, tc, encoded) - } -} - -func TestDecodeError(t *testing.T) { - testcases := [][]byte{ - mustDecodeHex("0xdc82013a0185012a05f2008504a817c8008080872386f26fc1000000"), - mustDecodeHex("0xdc013a01012a05f2008504a817c8008080872386f26fc1000000"), - mustDecodeHex("0xdc82013a0185012a05f28504a817c08080872386f26fc1000000"), - mustDecodeHex("0xdc82013a0185012a05f504a817c080872386ffc1000000"), - mustDecodeHex("0x013a018505f2008504a817c8008080872386f26fc1000000"), - } - - for _, tc := range testcases { - _, err := DecodeRLP(tc) - require.NotNil(t, err, hex.EncodeToString(tc)) - } -} - -func TestDecode1(t *testing.T) { - b := mustDecodeHex("0x02f8758401df5e7680832c8411832c8411830767f89452963ef50e27e06d72d59fcb4f3c2a687be3cfef880de0b6b3a764000080c080a094b11866f453ad85a980e0e8a2fc98cbaeb4409618c7734a7e12ae2f66fd405da042dbfb1b37af102023830ceeee0e703ffba0b8b3afeb8fe59f405eca9ed61072") - decoded, err := ParseEthTxArgs(b) - require.NoError(t, err) - - sender, err := decoded.Sender() - require.NoError(t, err) - - addr, err := address.NewFromString("f410fkkld55ioe7qg24wvt7fu6pbknb56ht7pt4zamxa") - require.NoError(t, err) - require.Equal(t, sender, addr) -}