diff --git a/.github/workflows/forest.yml b/.github/workflows/forest.yml index 79d1b61030d2..2b9a7edc9035 100644 --- a/.github/workflows/forest.yml +++ b/.github/workflows/forest.yml @@ -270,6 +270,33 @@ jobs: ./scripts/tests/calibnet_wallet_check.sh "$CALIBNET_WALLET" fi timeout-minutes: ${{ fromJSON(env.SCRIPT_TIMEOUT_MINUTES) }} + calibnet-delegated-wallet-check: + needs: + - build-ubuntu + name: Delegated wallet tests + runs-on: ubuntu-24.04 + steps: + - run: lscpu + - uses: actions/cache@v5 + with: + path: "${{ env.FIL_PROOFS_PARAMETER_CACHE }}" + key: proof-params-keys + - uses: actions/checkout@v6 + - uses: actions/download-artifact@v8 + with: + name: "forest-${{ runner.os }}" + path: ~/.cargo/bin + - name: Set permissions + run: | + chmod +x ~/.cargo/bin/forest* + - name: Delegated wallet commands check + env: + CALIBNET_WALLET: "${{ secrets.CALIBNET_WALLET }}" + run: | + if [[ "$CALIBNET_WALLET" != "" ]]; then + ./scripts/tests/calibnet_delegated_wallet_check.sh "$CALIBNET_WALLET" + fi + timeout-minutes: ${{ fromJSON(env.SCRIPT_TIMEOUT_MINUTES) }} calibnet-export-check-v1: if: ${{ github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'Release')) }} needs: diff --git a/CHANGELOG.md b/CHANGELOG.md index a30461341027..fae3bbb8d1f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,8 @@ ### Added +- [#6710](https://github.com/ChainSafe/forest/pull/6710): Added support for f4 addresses in forest-wallet. + ### Changed ### Removed diff --git a/scripts/tests/calibnet_delegated_wallet_check.sh b/scripts/tests/calibnet_delegated_wallet_check.sh new file mode 100755 index 000000000000..90790f0e8697 --- /dev/null +++ b/scripts/tests/calibnet_delegated_wallet_check.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash +# This script checks delegated wallet features of the forest node and the forest-cli. +# It requires both `forest` and `forest-cli` to be in the PATH. + +set -euxo pipefail + +source "$(dirname "$0")/harness.sh" + +forest_wallet_init "$@" + +# Amount to send (note: `send` command defaults to FIL if no units are specified) +FIL_AMT="500 atto FIL" + +# Amount for an empty wallet +FIL_ZERO="0 FIL" + +# The preloaded address +ADDR_ONE=$($FOREST_WALLET_PATH list | tail -1 | cut -d ' ' -f1) + +sleep 5s + +: Begin delegated wallet tests + +# The following steps do basic delegated wallet handling tests. + +echo "Creating delegated wallet DELEGATE_ADDR_ONE" +DELEGATE_ADDR_ONE=$($FOREST_WALLET_PATH new delegated) +echo "$DELEGATE_ADDR_ONE" +$FOREST_WALLET_PATH export "$DELEGATE_ADDR_ONE" > delegated_wallet.key +$FOREST_WALLET_PATH --remote-wallet import delegated_wallet.key + +# Fund delegated wallet from preloaded wallet +DELEGATE_FUND_AMT="2 micro FIL" +$FOREST_WALLET_PATH set-default "$ADDR_ONE" +MSG_DELEGATE_FUND=$($FOREST_WALLET_PATH send "$DELEGATE_ADDR_ONE" "$DELEGATE_FUND_AMT") +: "$MSG_DELEGATE_FUND" + +DELEGATE_ADDR_ONE_BALANCE=$FIL_ZERO +i=0 +while [[ $i != 20 && $DELEGATE_ADDR_ONE_BALANCE == "$FIL_ZERO" ]]; do + i=$((i+1)) + : "Checking DELEGATE_ADDR_ONE balance $i/20" + sleep 30s + DELEGATE_ADDR_ONE_BALANCE=$($FOREST_WALLET_PATH balance "$DELEGATE_ADDR_ONE" --exact-balance) +done + +echo "Creating delegated wallet DELEGATE_ADDR_TWO" +DELEGATE_ADDR_TWO=$($FOREST_WALLET_PATH new delegated) +echo "$DELEGATE_ADDR_TWO" +$FOREST_WALLET_PATH set-default "$DELEGATE_ADDR_ONE" + +echo "Creating delegated (remote) wallet DELEGATE_ADDR_THREE" +DELEGATE_ADDR_THREE=$($FOREST_WALLET_PATH --remote-wallet new delegated) +echo "$DELEGATE_ADDR_THREE" +$FOREST_WALLET_PATH --remote-wallet set-default "$DELEGATE_ADDR_ONE" + +$FOREST_WALLET_PATH list +$FOREST_WALLET_PATH --remote-wallet list + +MSG_DELEGATE_TWO=$($FOREST_WALLET_PATH send "$DELEGATE_ADDR_TWO" "$FIL_AMT") +: "$MSG_DELEGATE_TWO" + +MSG_DELEGATE_THREE=$($FOREST_WALLET_PATH send "$DELEGATE_ADDR_THREE" "$FIL_AMT") +: "$MSG_DELEGATE_THREE" + +DELEGATE_ADDR_TWO_BALANCE=$FIL_ZERO +i=0 +while [[ $i != 20 && $DELEGATE_ADDR_TWO_BALANCE == "$FIL_ZERO" ]]; do + i=$((i+1)) + : "Checking DELEGATE_ADDR_TWO balance $i/20" + sleep 30s + DELEGATE_ADDR_TWO_BALANCE=$($FOREST_WALLET_PATH balance "$DELEGATE_ADDR_TWO" --exact-balance) +done + +DELEGATE_ADDR_THREE_BALANCE=$FIL_ZERO +i=0 +while [[ $i != 20 && $DELEGATE_ADDR_THREE_BALANCE == "$FIL_ZERO" ]]; do + i=$((i+1)) + : "Checking DELEGATE_ADDR_THREE balance $i/20" + sleep 30s + DELEGATE_ADDR_THREE_BALANCE=$($FOREST_WALLET_PATH --remote-wallet balance "$DELEGATE_ADDR_THREE" --exact-balance) +done + +$FOREST_WALLET_PATH list +$FOREST_WALLET_PATH --remote-wallet list + +: End delegated wallet tests diff --git a/scripts/tests/calibnet_wallet_check.sh b/scripts/tests/calibnet_wallet_check.sh index 0bd3def687ed..550c5604e3c4 100755 --- a/scripts/tests/calibnet_wallet_check.sh +++ b/scripts/tests/calibnet_wallet_check.sh @@ -7,23 +7,7 @@ set -euxo pipefail source "$(dirname "$0")/harness.sh" - -usage() { - echo "Usage: $0 " - exit 1 -} - -if [ -z "$1" ] - then - usage -fi - -echo "$1" > preloaded_wallet.key - -forest_init "$@" - -$FOREST_WALLET_PATH import preloaded_wallet.key -$FOREST_WALLET_PATH --remote-wallet import preloaded_wallet.key +forest_wallet_init "$@" # Test commented out due to it being flaky. See the tracking issue: https://github.com/ChainSafe/forest/issues/4849 # : Begin Filecoin.MarketAddBalance test @@ -115,6 +99,8 @@ sleep 5s # Show balances $FOREST_WALLET_PATH list +FOREST_URL="http://127.0.0.1:2345/rpc/v1" + echo "Creating a new address to send FIL to" ADDR_TWO=$($FOREST_WALLET_PATH new) echo "$ADDR_TWO" @@ -154,6 +140,46 @@ while [[ $i != 20 && $ADDR_THREE_BALANCE == "$FIL_ZERO" ]]; do ADDR_THREE_BALANCE=$($FOREST_WALLET_PATH --remote-wallet balance "$ADDR_THREE" --exact-balance) done +ETH_ADDR_TWO=$(curl -s -X POST "$FOREST_URL" \ + -H 'Content-Type: application/json' \ + -H "Authorization: Bearer $ADMIN_TOKEN" \ + --data "$(jq -n --arg addr "$ADDR_TWO" '{jsonrpc: "2.0", id: 1, method: "Filecoin.FilecoinAddressToEthAddress", params: [$addr, "pending"]}')" \ + | jq -r '.result') +echo "ETH address: $ETH_ADDR_TWO" + +ETH_ADDR_THREE=$(curl -s -X POST "$FOREST_URL" \ + -H 'Content-Type: application/json' \ + -H "Authorization: Bearer $ADMIN_TOKEN" \ + --data "$(jq -n --arg addr "$ADDR_THREE" '{jsonrpc: "2.0", id: 1, method: "Filecoin.FilecoinAddressToEthAddress", params: [$addr, "pending"]}')" \ + | jq -r '.result') +echo "ETH address: $ETH_ADDR_THREE" + +MSG_ETH=$($FOREST_WALLET_PATH send "$ETH_ADDR_TWO" "$FIL_AMT") +: "$MSG_ETH" + +MSG_ETH_REMOTE=$($FOREST_WALLET_PATH --remote-wallet send "$ETH_ADDR_THREE" "$FIL_AMT") +: "$MSG_ETH_REMOTE" + +ETH_ADDR_TWO_BALANCE=$ADDR_TWO_BALANCE +i=0 +while [[ $i != 20 && $ETH_ADDR_TWO_BALANCE == "$ADDR_TWO_BALANCE" ]]; do + i=$((i+1)) + + : "Checking balance $i/20" + sleep 30s + ETH_ADDR_TWO_BALANCE=$($FOREST_WALLET_PATH balance "$ADDR_TWO" --exact-balance) +done + +ETH_ADDR_THREE_BALANCE=$ADDR_THREE_BALANCE +i=0 +while [[ $i != 20 && $ETH_ADDR_THREE_BALANCE == "$ADDR_THREE_BALANCE" ]]; do + i=$((i+1)) + + : "Checking balance $i/20" + sleep 30s + ETH_ADDR_THREE_BALANCE=$($FOREST_WALLET_PATH --remote-wallet balance "$ADDR_THREE" --exact-balance) +done + # wallet list should contain address two with transferred FIL amount $FOREST_WALLET_PATH list $FOREST_WALLET_PATH --remote-wallet list diff --git a/scripts/tests/harness.sh b/scripts/tests/harness.sh index f949b7700ab6..0ab83f6399c4 100644 --- a/scripts/tests/harness.sh +++ b/scripts/tests/harness.sh @@ -141,6 +141,25 @@ function forest_init_stateless { export FULLNODE_API_INFO } +function forest_wallet_init { + usage() { + echo "Usage: $0 " + exit 1 + } + + if [ -z "$1" ] + then + usage + fi + + echo "$1" > preloaded_wallet.key + + forest_init "$@" + + $FOREST_WALLET_PATH import preloaded_wallet.key + $FOREST_WALLET_PATH --remote-wallet import preloaded_wallet.key +} + function forest_print_logs_and_metrics { echo "Get and print metrics" wget -O metrics.log http://localhost:6116/metrics diff --git a/src/message_pool/msgpool/msg_pool.rs b/src/message_pool/msgpool/msg_pool.rs index 48c3c9caa69f..6333df24debd 100644 --- a/src/message_pool/msgpool/msg_pool.rs +++ b/src/message_pool/msgpool/msg_pool.rs @@ -16,8 +16,9 @@ use crate::eth::is_valid_eth_tx_for_sending; use crate::libp2p::{NetworkMessage, PUBSUB_MSG_STR, Topic}; use crate::message::{ChainMessage, Message, SignedMessage, valid_for_block_inclusion}; use crate::networks::{ChainConfig, NEWEST_NETWORK_VERSION}; +use crate::rpc::eth::types::EthAddress; use crate::shim::{ - address::Address, + address::{Address, Protocol}, crypto::{Signature, SignatureType}, econ::TokenAmount, gas::{Gas, price_list_by_network_version}, @@ -265,6 +266,12 @@ where if to_vec(msg)?.len() > MAX_MESSAGE_SIZE { return Err(Error::MessageTooBig); } + let to = msg.message().to(); + if to.protocol() == Protocol::Delegated { + EthAddress::from_filecoin_address(&to).context(format!( + "message recipient {to} is a delegated address but not a valid Eth Address" + ))?; + } valid_for_block_inclusion(msg.message(), Gas::new(0), NEWEST_NETWORK_VERSION)?; if msg.value() > *crate::shim::econ::TOTAL_FILECOIN { return Err(Error::MessageValueTooHigh); diff --git a/src/rpc/methods/eth.rs b/src/rpc/methods/eth.rs index 47a88cbee672..75514171ef38 100644 --- a/src/rpc/methods/eth.rs +++ b/src/rpc/methods/eth.rs @@ -1012,7 +1012,7 @@ async fn execute_tipset( )) } -fn is_eth_address(addr: &VmAddress) -> bool { +pub fn is_eth_address(addr: &VmAddress) -> bool { if addr.protocol() != Protocol::Delegated { return false; } diff --git a/src/wallet/subcommands/wallet_cmd.rs b/src/wallet/subcommands/wallet_cmd.rs index 1003ac31a96c..62cb3f6c8fa2 100644 --- a/src/wallet/subcommands/wallet_cmd.rs +++ b/src/wallet/subcommands/wallet_cmd.rs @@ -12,12 +12,17 @@ use crate::key_management::{Key, KeyInfo}; use crate::{ ENCRYPTED_KEYSTORE_NAME, cli::humantoken, + eth::{EAMMethod, EVMMethod, EthEip1559TxArgsBuilder, EthTx}, message::SignedMessage, rpc::{ + eth::{EthChainId, is_eth_address, types::EthAddress}, mpool::{MpoolGetNonce, MpoolPush, MpoolPushMessage}, types::ApiTipsetKey, }, - shim::address::Address, + shim::{ + address::{Address, Protocol}, + message::{METHOD_SEND, Message}, + }, }; use crate::{KeyStore, lotus_json::LotusJson}; use crate::{ @@ -26,7 +31,6 @@ use crate::{ address::StrictAddress, crypto::{Signature, SignatureType}, econ::TokenAmount, - message::{METHOD_SEND, Message}, }, }; use crate::{ @@ -499,11 +503,28 @@ impl WalletCommands { .into() }; + let (mut to, is_0x_recipient) = resolve_target_address(&target_address)?; + + // Resolve to ID address when sending from delegated address to non-ID/non-Delegated address. + if is_eth_address(&from) + && to.protocol() != Protocol::ID + && to.protocol() != Protocol::Delegated + { + to = StateLookupID::call(&backend.remote, (to, ApiTipsetKey(None))) + .await + .with_context(|| { + format!( + "addresses starting with f410f can only send to other addresses starting with f410f, or id addresses. could not find id address for {to}" + ) + })?; + } + let method_num = resolve_method_num(&from, &to, is_0x_recipient); + let message = Message { from, - to: StrictAddress::from_str(&target_address)?.into(), + to, value: amount, - method_num: METHOD_SEND, + method_num, gas_limit: gas_limit as u64, gas_fee_cap: gas_feecap, gas_premium, @@ -526,13 +547,34 @@ impl WalletCommands { message.sequence = MpoolGetNonce::call(&backend.remote, (from,)).await?; let key = crate::key_management::find_key(&from, keystore)?; - let sig = crate::key_management::sign( - *key.key_info.key_type(), - key.key_info.private_key(), - message.cid().to_bytes().as_slice(), - )?; - - let smsg = SignedMessage::new_from_parts(message, sig)?; + let sig_type = *key.key_info.key_type(); + let smsg = if sig_type == SignatureType::Delegated { + let eth_chain_id = u64::from_str_radix( + EthChainId::call(&backend.remote, ()) + .await? + .trim_start_matches("0x"), + 16, + )?; + let eth_tx_args = EthEip1559TxArgsBuilder::default() + .chain_id(eth_chain_id) + .unsigned_message(&message)? + .build()?; + let eth_tx = EthTx::Eip1559(Box::new(eth_tx_args)); + let sig = crate::key_management::sign( + sig_type, + key.key_info.private_key(), + ð_tx.rlp_unsigned_message(eth_chain_id)?, + )?; + let unsigned_msg = eth_tx.get_unsigned_message(from, eth_chain_id)?; + SignedMessage::new_unchecked(unsigned_msg, sig) + } else { + let sig = crate::key_management::sign( + sig_type, + key.key_info.private_key(), + message.cid().to_bytes().as_slice(), + )?; + SignedMessage::new_from_parts(message, sig)? + }; MpoolPush::call(&backend.remote, (smsg.clone(),)).await?; smsg @@ -595,3 +637,155 @@ fn format_balance(balance: &TokenAmount, no_round: bool, no_abbrev: bool) -> Str (false, false) => format!("{:.4}", balance.pretty()), } } + +fn resolve_target_address(target_address: &str) -> anyhow::Result<(Address, bool)> { + match StrictAddress::from_str(target_address) { + Ok(addr) => Ok((addr.into(), false)), + Err(_) => { + let eth_addr = EthAddress::from_str(target_address) + .context("target address must be a valid FIL address or ETH address (0x...)")?; + let addr = eth_addr.to_filecoin_address()?; + Ok((addr, true)) + } + } +} + +fn resolve_method_num(from: &Address, to: &Address, is_0x_recipient: bool) -> u64 { + if !is_eth_address(from) && !is_0x_recipient { + return METHOD_SEND; + } + if *to == Address::ETHEREUM_ACCOUNT_MANAGER_ACTOR { + EAMMethod::CreateExternal as u64 + } else { + EVMMethod::InvokeContract as u64 + } +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use crate::eth::{EAMMethod, EVMMethod}; + use crate::rpc::eth::types::EthAddress; + use crate::shim::address::{Address, CurrentNetwork, Network}; + use crate::shim::message::METHOD_SEND; + + use super::{resolve_method_num, resolve_target_address}; + + #[test] + fn test_resolve_target_address_id() { + CurrentNetwork::with(Network::Mainnet, || { + let (addr, is_0x) = resolve_target_address("f01234").unwrap(); + assert!(!is_0x); + let expected_addr = Address::new_id(1234); + assert_eq!(addr, expected_addr); + }); + CurrentNetwork::with(Network::Testnet, || { + let (addr, is_0x) = resolve_target_address("t01234").unwrap(); + assert!(!is_0x); + let expected_addr = Address::new_id(1234); + assert_eq!(addr, expected_addr); + }); + } + + #[test] + fn test_resolve_target_address_masked_id() { + CurrentNetwork::with(Network::Mainnet, || { + let (addr, is_0x) = + resolve_target_address("0xff000000000000000000000000000000000004d2").unwrap(); + assert!(is_0x); + let expected_addr = Address::new_id(1234); + assert_eq!(addr, expected_addr); + }); + CurrentNetwork::with(Network::Testnet, || { + let (addr, is_0x) = + resolve_target_address("0xff000000000000000000000000000000000004d2").unwrap(); + assert!(is_0x); + let expected_addr = Address::new_id(1234); + assert_eq!(addr, expected_addr); + }); + } + + #[test] + fn test_resolve_target_address_eth() { + CurrentNetwork::with(Network::Mainnet, || { + let (addr, is_0x) = + resolve_target_address("0x6cb414224f0b91de5c3b616e700e34a5172c149f").unwrap(); + assert!(is_0x); + let expected_addr = + Address::from_str("f410fns2biispboi54xb3mfxhadruuulsyfe73avfmey").unwrap(); + assert_eq!(addr, expected_addr); + }); + CurrentNetwork::with(Network::Testnet, || { + let (addr, is_0x) = + resolve_target_address("0x6cb414224f0b91de5c3b616e700e34a5172c149f").unwrap(); + assert!(is_0x); + let expected_addr = + Address::from_str("t410fns2biispboi54xb3mfxhadruuulsyfe73avfmey").unwrap(); + assert_eq!(addr, expected_addr); + }); + } + + #[test] + fn test_resolve_target_address_invalid() { + let err = resolve_target_address("0xInvalidAddress").unwrap_err(); + assert!( + err.to_string() + .contains("target address must be a valid FIL address or ETH address") + ); + } + + #[test] + fn test_resolve_method_num_send() { + let from = Address::from_str("f01234").unwrap(); + let to = Address::from_str("f01234").unwrap(); + let method = resolve_method_num(&from, &to, false); + assert_eq!(method, METHOD_SEND); + } + + #[test] + fn test_resolve_method_num_create_external() { + let from = Address::from_str("f410fvfpyxvy6aqet3g2bfbj6h7nr5kjgyncpaeimgxa").unwrap(); + let to = Address::ETHEREUM_ACCOUNT_MANAGER_ACTOR; + let method = resolve_method_num(&from, &to, false); + assert_eq!(method, EAMMethod::CreateExternal as u64); + } + + #[test] + fn test_resolve_method_num_invoke_contract() { + let from = Address::from_str("f410fvfpyxvy6aqet3g2bfbj6h7nr5kjgyncpaeimgxa").unwrap(); + let to = Address::from_str("f410fvfpyxvy6aqet3g2bfbj6h7nr5kjgyncpaeimgxa").unwrap(); + let method = resolve_method_num(&from, &to, false); + assert_eq!(method, EVMMethod::InvokeContract as u64); + } + + #[test] + fn test_resolve_method_num_invoke_contract_eth() { + let from = Address::from_str("f410fvfpyxvy6aqet3g2bfbj6h7nr5kjgyncpaeimgxa").unwrap(); + let to = EthAddress::from_str("0x6cb414224f0b91de5c3b616e700e34a5172c149f") + .unwrap() + .to_filecoin_address() + .unwrap(); + let method = resolve_method_num(&from, &to, true); + assert_eq!(method, EVMMethod::InvokeContract as u64); + } + + #[test] + fn test_resolve_method_num_send_to_delegated() { + let from = Address::from_str("f01234").unwrap(); + let to = Address::from_str("f410fvfpyxvy6aqet3g2bfbj6h7nr5kjgyncpaeimgxa").unwrap(); + let method = resolve_method_num(&from, &to, false); + assert_eq!(method, METHOD_SEND); + } + + #[test] + fn test_resolve_method_num_send_to_eth() { + let from = Address::from_str("f01234").unwrap(); + let to = EthAddress::from_str("0x6cb414224f0b91de5c3b616e700e34a5172c149f") + .unwrap() + .to_filecoin_address() + .unwrap(); + let method = resolve_method_num(&from, &to, true); + assert_eq!(method, EVMMethod::InvokeContract as u64); + } +}