Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion .github/workflows/forest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,14 @@ jobs:
- name: Migration Regression Tests
run: ./scripts/tests/calibnet_migration_regression_tests.sh
timeout-minutes: ${{ fromJSON(env.SCRIPT_TIMEOUT_MINUTES) }}
calibnet-wallet-check-no-ops:
name: Wallet tests
runs-on: ubuntu-slim
if: ${{ !contains(github.event.pull_request.labels.*.name, 'Wallet') }}
steps:
- run: echo "No-op job to trigger the required wallet tests."
calibnet-wallet-check:
if: ${{ contains(github.event.pull_request.labels.*.name, 'Wallet') }}
needs:
- build-ubuntu
name: Wallet tests
Expand Down Expand Up @@ -480,6 +487,18 @@ jobs:
- name: Devnet check
run: ./scripts/devnet/check.sh
timeout-minutes: ${{ fromJSON(env.SCRIPT_TIMEOUT_MINUTES) }}
- name: Add forest binaries to PATH
run: |
chmod +x "$GITHUB_WORKSPACE"/forest*
echo "$GITHUB_WORKSPACE" >> "$GITHUB_PATH"
- name: Devnet wallet tests
run: |
set -euo pipefail
source ./scripts/devnet/wallet_harness.sh
devnet_wallet_env_init
forest-dev tests devnet mpool
forest-dev tests devnet wallet
timeout-minutes: ${{ fromJSON(env.SCRIPT_TIMEOUT_MINUTES) }}
- name: Dump docker logs
if: always()
uses: jwalton/gh-docker-logs@v2
Expand Down Expand Up @@ -574,7 +593,6 @@ jobs:
- calibnet-stateless-mode-check
- calibnet-stateless-rpc-check
- state-migrations-check
- calibnet-wallet-check
- calibnet-no-discovery-checks
- calibnet-kademlia-checks
- calibnet-eth-mapping-check
Expand Down
15 changes: 15 additions & 0 deletions mise.toml
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,21 @@ forest-dev tests calibnet mpool
forest-dev tests calibnet wallet
'''

[tasks."test:wallet-devnet"]
description = "Run wallet integration tests against a local docker devnet."
shell = "bash -c"
run = '''
set -euo pipefail
pushd scripts/devnet
./setup.sh
./check.sh
popd
source ./scripts/devnet/wallet_harness.sh
devnet_wallet_env_init
forest-dev tests devnet mpool
forest-dev tests devnet wallet
'''

[tasks."codecov:nextest"]
description = "Run codecov with nextest"
run = '''
Expand Down
17 changes: 16 additions & 1 deletion scripts/devnet/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,26 @@ and setup credentials. Then run any command:

```shell
export TOKEN=$(cat /forest_data/token.jwt)
export FULLNODE_API_INFO=$TOKEN:/dns/forest/tcp/1234/http
export FULLNODE_API_INFO=$TOKEN:/dns/forest/tcp/3456/http

forest-cli net peers
```

## Running the wallet integration tests

The same wallet/mpool integration suite that runs against calibnet can be run
against the local devnet. This brings up the devnet, waits for it to sync, wires
up the host environment, and runs the tests:

```shell
mise run test:wallet-devnet
```

Under the hood this sources `wallet_harness.sh`, which reads the admin token and
the funded genesis key from the running `forest` container, exports
`FULLNODE_API_INFO` (Forest RPC on port 3456) and `FOREST_TEST_PRELOADED_ADDRESS`,
then runs `forest-dev tests devnet mpool` and `forest-dev tests devnet wallet`.

## Local devnet development

If you prefer to have Forest running directly on the host, you can comment it
Expand Down
56 changes: 56 additions & 0 deletions scripts/devnet/wallet_harness.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#!/bin/bash
# Sourced (not executed) helpers for the wallet/mpool suite on the docker
# devnet. Run after the devnet is up (`setup.sh`) and synced (`check.sh`).
#
# The genesis key is the Lotus miner's default wallet, so using it as the test
# sender causes nonce contention. We fund a dedicated wallet instead.

WALLET_HARNESS_PARENT_PATH=$( cd "$(dirname "${BASH_SOURCE[0]}")" ; pwd -P )
source "${WALLET_HARNESS_PARENT_PATH}/.env"

export FOREST_CLI_PATH="${FOREST_CLI_PATH:-forest-cli}"
export FOREST_WALLET_PATH="${FOREST_WALLET_PATH:-forest-wallet}"
export DEVNET_TEST_FUND_AMT="${DEVNET_TEST_FUND_AMT:-100 FIL}"

function devnet_wallet_env_init {
local token
token=$(docker exec forest cat "${FOREST_DATA_DIR}/token.jwt")
export FULLNODE_API_INFO="${token}:/ip4/127.0.0.1/tcp/${FOREST_RPC_PORT}/http"

local tmp="${TMPDIR:-/tmp}"

# Derive the genesis address via a throwaway keystore (`XDG_DATA_HOME`) so it
# never lands in the real keystores.
local genesis_key_path="${tmp}/devnet_genesis_wallet.key"
docker cp "forest:${LOTUS_DATA_DIR}/genesis-sectors/pre-seal-${MINER_ACTOR_ADDRESS}.key" "${genesis_key_path}"
local genesis_addr
genesis_addr="$(XDG_DATA_HOME="$(mktemp -d)" ${FOREST_WALLET_PATH} import "${genesis_key_path}")"

# Fresh sender the miner never touches; mirror to the remote keystore so both
# `Backend::Local` and `Backend::Remote` variants work.
local test_addr test_key_path
test_addr="$(${FOREST_WALLET_PATH} new)"
test_key_path="${tmp}/devnet_test_wallet.key"
${FOREST_WALLET_PATH} export "${test_addr}" > "${test_key_path}"
${FOREST_WALLET_PATH} --remote-wallet import "${test_key_path}"
export FOREST_TEST_PRELOADED_ADDRESS="${test_addr}"

echo "Devnet wallet env initialised:"
echo " FULLNODE_API_INFO=<token>:/ip4/127.0.0.1/tcp/${FOREST_RPC_PORT}/http"
echo " FOREST_TEST_PRELOADED_ADDRESS=${FOREST_TEST_PRELOADED_ADDRESS}"
echo " Funding ${test_addr} with ${DEVNET_TEST_FUND_AMT} from ${genesis_addr}..."

# Fund it from the genesis key and wait for the transfer.
${FOREST_WALLET_PATH} --remote-wallet send --wait-confidence 0 --wait-timeout 10m \
--from "${genesis_addr}" "${test_addr}" "${DEVNET_TEST_FUND_AMT}"

local balance
balance="$(${FOREST_WALLET_PATH} --remote-wallet balance "${test_addr}" --exact-balance)" || balance=""
if [[ -z "${balance}" || "${balance}" == "0 FIL" ]]; then
echo "ERROR: dedicated test wallet ${test_addr} was not funded in time" >&2
return 1
fi

${FOREST_CLI_PATH} chain head
echo " balance=${balance}"
}
10 changes: 7 additions & 3 deletions src/dev/subcommands/tests_cmd.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
// Copyright 2019-2026 ChainSafe Systems
// SPDX-License-Identifier: Apache-2.0, MIT

mod calibnet;
mod shared;

/// Integration tests
#[derive(Debug, clap::Subcommand)]
pub enum TestsCommand {
#[command(subcommand)]
Calibnet(calibnet::CalibnetTestsCommand),
Calibnet(shared::TestCommand),
/// Run the wallet/mpool integration suite against a local devnet. The tests
/// themselves are chain-agnostic, so the same suite is reused.
#[command(subcommand)]
Devnet(shared::TestCommand),
}

impl TestsCommand {
pub async fn run(self) -> anyhow::Result<()> {
match self {
Self::Calibnet(cmd) => cmd.run().await,
Self::Calibnet(cmd) | Self::Devnet(cmd) => cmd.run().await,
}
}
}
76 changes: 0 additions & 76 deletions src/dev/subcommands/tests_cmd/calibnet/mpool.rs

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@ mod helpers;
mod mpool;
mod wallet;

/// Calibnet integration tests
/// Shared integration tests (used by both calibnet and devnet)
#[derive(Debug, clap::Subcommand)]
pub enum CalibnetTestsCommand {
Wallet(wallet::CalibnetWalletTestCommand),
Mpool(mpool::CalibnetMpoolTestCommand),
pub enum TestCommand {
Wallet(wallet::WalletTestCommand),
Mpool(mpool::MpoolTestCommand),
}

impl CalibnetTestsCommand {
impl TestCommand {
pub async fn run(self) -> anyhow::Result<()> {
match self {
Self::Wallet(cmd) => cmd.run().await,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ pub const FIL_AMT: &str = "500 atto FIL";
/// Sentinel `forest-wallet balance --exact-balance` returns for an unfunded address.
pub const FIL_ZERO: &str = "0 FIL";
/// Amount to seed a freshly-created delegated wallet.
pub const DELEGATE_FUND_AMT: &str = "3 micro FIL";
pub const DELEGATE_FUND_AMT: &str = "30 micro FIL";

/// Maximum time to wait for a polled condition before failing the test.
pub const POLL_TIMEOUT: Duration = Duration::from_secs(600);
Expand Down Expand Up @@ -136,7 +136,7 @@ fn send_from_and_maybe_wait(
) -> anyhow::Result<String> {
let mut args = vec!["send", to, amount, "--from", from];
if wait {
args.extend(["--wait-confidence", "0", "--wait-timeout", "1m"]);
args.extend(["--wait-confidence", "0", "--wait-timeout", "10m"]);
}
let mut attempt = 1;
loop {
Expand Down
87 changes: 87 additions & 0 deletions src/dev/subcommands/tests_cmd/shared/mpool.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// Copyright 2019-2026 ChainSafe Systems
// SPDX-License-Identifier: Apache-2.0, MIT

//! Mpool CLI integration tests (shared preloaded address).
//!
//! Run the mpool suite before the wallet suite; see `mise test:wallet`.
//! Each test assumes the same environment as the wallet suite.

use super::helpers::*;
use libtest_mimic::{Arguments, Trial};

/// Mpool integration tests
#[derive(Debug, clap::Args)]
pub struct MpoolTestCommand {}

impl MpoolTestCommand {
pub async fn run(self) -> anyhow::Result<()> {
let args = Arguments {
test_threads: Some(1),
..Default::default()
};
libtest_mimic::run(&args, tests()).exit();
}
}

fn tests() -> Vec<Trial> {
vec![
Trial::test("mpool_nonce_fix_auto_unblocks_pending", || {
block_on(mpool_nonce_fix_auto_unblocks_pending());
Ok(())
}),
Trial::test("mpool_replace_auto_unblocks_pending", || {
block_on(mpool_replace_auto_unblocks_pending());
Ok(())
}),
]
}

async fn mpool_nonce_fix_auto_unblocks_pending() {
let addr = FOREST_TEST_PRELOADED_ADDRESS.as_str();
let nonce = mpool_nonce(addr).unwrap();
// Skip one nonce so `--auto` has a gap to fill.
let next_nonce = nonce + 1;
forest_cli(&[
"mpool",
"nonce-fix",
"--addr",
addr,
"--start",
&next_nonce.to_string(),
"--end",
&(next_nonce + 1).to_string(),
])
.unwrap();
poll_until_pending_nonce(addr, next_nonce).await.unwrap();

forest_cli(&["mpool", "nonce-fix", "--addr", addr, "--auto"]).unwrap();

assert!(
poll_until_pending_nonce(addr, nonce).await.is_ok(),
"nonce-fix --auto should fill nonce gap at {nonce} for {addr}."
);
}

async fn mpool_replace_auto_unblocks_pending() {
let addr = FOREST_TEST_PRELOADED_ADDRESS.as_str();
let nonce = mpool_nonce(addr).unwrap();

let cid = send_from_no_wait(addr, addr, FIL_AMT, Backend::Local).unwrap();
poll_until_pending_nonce(addr, nonce).await.unwrap();

forest_cli(&[
"mpool",
"replace",
"--from",
addr,
"--nonce",
&nonce.to_string(),
"--auto",
])
.unwrap();

assert!(
poll_until_state_search_msg(&cid).await.is_ok(),
"mpool replace --auto should replace message {cid} from {addr} at nonce {nonce}."
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@
use super::helpers::*;
use libtest_mimic::{Arguments, Trial};

/// Calibnet wallet integration tests
/// Wallet integration tests
#[derive(Debug, clap::Args)]
pub struct CalibnetWalletTestCommand {}
pub struct WalletTestCommand {}

impl CalibnetWalletTestCommand {
impl WalletTestCommand {
pub async fn run(self) -> anyhow::Result<()> {
let args = Arguments {
test_threads: Some(8),
Expand Down
Loading
Loading