diff --git a/.changelog/unreleased/CI/4140-ledger-namada-unit-tests-ci.md b/.changelog/unreleased/CI/4140-ledger-namada-unit-tests-ci.md new file mode 100644 index 0000000000..3bde9b2c7e --- /dev/null +++ b/.changelog/unreleased/CI/4140-ledger-namada-unit-tests-ci.md @@ -0,0 +1,2 @@ +- Add Ledger app unit tests to the Namada CI. + ([\#4140](https://github.com/anoma/namada/pull/4140)) \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 264b8342ac..8999fa3210 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,6 +35,7 @@ env: AWS_REGION: us-west-2 NIGHTLY: nightly-2024-09-08 NAMADA_MASP_PARAMS_DIR: /masp/.masp-params + LEDGER_APP_VERSION: "1.0.6-ci-patch" jobs: changelog: @@ -651,6 +652,45 @@ jobs: fail_ci_if_error: true token: ${{ secrets.CODECOV_TOKEN }} + test-ledger-app: + timeout-minutes: 30 + runs-on: [self-hosted, 4vcpu-8ram-ubuntu22-namada-x86] + + steps: + - name: Checkout repo + uses: actions/checkout@v4 + if: ${{ github.event_name != 'pull_request_target' }} + - name: Checkout PR + uses: actions/checkout@v4 + if: ${{ github.event_name == 'pull_request_target' }} + with: + ref: ${{ github.event.pull_request.head.sha }} + - name: Configure AWS + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-region: ${{ env.AWS_REGION }} + role-to-assume: ${{ secrets.AWS_ROLE }} + - name: Checkout ledger-namada + run: | + echo "Using Namada Ledger App version: v${LEDGER_APP_VERSION}" + git clone 'https://github.com/heliaxdev/ledger-namada' ../ledger-namada + cd ../ledger-namada + git checkout "v$LEDGER_APP_VERSION" + git submodule update --init --recursive + sudo update-alternatives --install /usr/bin/python python /usr/bin/python3 10 + make deps + - name: Generate test vectors + run: | + # The path where the Ledger app test suite will locate test vectors + TESTVEC_PATH="../ledger-namada/tests/testvectors.json" + TESTDBG_PATH="../ledger-namada/tests/testdebugs.txt" + sudo apt-get install -y protobuf-compiler + cargo run --example generate-txs -- $TESTVEC_PATH $TESTDBG_PATH + - name: Check test vectors + run: | + cd ../ledger-namada + make cpp_test + test-e2e-with-device-automation: runs-on: [self-hosted, 4vcpu-8ram-ubuntu22-namada-x86] container: diff --git a/crates/ibc/src/lib.rs b/crates/ibc/src/lib.rs index 74830fda51..a3e3031125 100644 --- a/crates/ibc/src/lib.rs +++ b/crates/ibc/src/lib.rs @@ -1284,8 +1284,8 @@ pub mod testing { prop_compose! { /// Generate an arbitrary IBC token ID vector - pub fn arb_ibc_token_ids()(token_ids in collection::vec(arb_ibc_token_id(), 1..10)) -> TokenIds { - TokenIds(token_ids) + pub fn arb_ibc_token_ids()(token_ids in collection::vec(arb_ibc_token_id().prop_map(|x| x.to_string()), 1..10)) -> TokenIds { + TokenIds::try_from(token_ids).expect("generated invalid IBC token ID vector") } } diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs index 52f0eb3398..73f6907d30 100644 --- a/crates/sdk/src/lib.rs +++ b/crates/sdk/src/lib.rs @@ -1085,11 +1085,11 @@ pub mod testing { prop_compose! { /// Generate an arbitrary header - pub fn arb_header()( + pub fn arb_header(cmt_count: impl Into,)( chain_id in arb_chain_id(), expiration in option::of(arb_date_time_utc()), timestamp in arb_date_time_utc(), - batch in arb_tx_commitments(1..10), + batch in arb_tx_commitments(cmt_count), atomic in proptest::bool::ANY, tx_type in arb_tx_type(), ) -> Header { @@ -1123,7 +1123,7 @@ pub mod testing { prop_compose! { /// Generate an arbitrary masp transfer transaction pub fn arb_transfer_tx()( - mut header in arb_header(), + mut header in arb_header(0), wrapper in arb_wrapper_tx(), code_hash in arb_hash(), (transfer, aux) in arb_transfer(), @@ -1156,7 +1156,7 @@ pub mod testing { prop_compose! { /// Generate an arbitrary bond transaction pub fn arb_bond_tx()( - mut header in arb_header(), + mut header in arb_header(0), wrapper in arb_wrapper_tx(), bond in arb_bond(), code_hash in arb_hash(), @@ -1172,7 +1172,7 @@ pub mod testing { prop_compose! { /// Generate an arbitrary bond transaction pub fn arb_unbond_tx()( - mut header in arb_header(), + mut header in arb_header(0), wrapper in arb_wrapper_tx(), unbond in arb_bond(), code_hash in arb_hash(), @@ -1188,7 +1188,7 @@ pub mod testing { prop_compose! { /// Generate an arbitrary account initialization transaction pub fn arb_init_account_tx()( - mut header in arb_header(), + mut header in arb_header(0), wrapper in arb_wrapper_tx(), mut init_account in arb_init_account(), extra_data in arb_code(), @@ -1207,7 +1207,7 @@ pub mod testing { prop_compose! { /// Generate an arbitrary account initialization transaction pub fn arb_become_validator_tx()( - mut header in arb_header(), + mut header in arb_header(0), wrapper in arb_wrapper_tx(), become_validator in arb_become_validator(), code_hash in arb_hash(), @@ -1223,7 +1223,7 @@ pub mod testing { prop_compose! { /// Generate an arbitrary proposal initialization transaction pub fn arb_init_proposal_tx()( - mut header in arb_header(), + mut header in arb_header(0), wrapper in arb_wrapper_tx(), mut init_proposal in arb_init_proposal(), content_extra_data in arb_code(), @@ -1272,7 +1272,7 @@ pub mod testing { prop_compose! { /// Generate an arbitrary vote proposal transaction pub fn arb_vote_proposal_tx()( - mut header in arb_header(), + mut header in arb_header(0), wrapper in arb_wrapper_tx(), vote_proposal in arb_vote_proposal(), code_hash in arb_hash(), @@ -1288,7 +1288,7 @@ pub mod testing { prop_compose! { /// Generate an arbitrary reveal public key transaction pub fn arb_reveal_pk_tx()( - mut header in arb_header(), + mut header in arb_header(0), wrapper in arb_wrapper_tx(), pk in arb_common_pk(), code_hash in arb_hash(), @@ -1304,7 +1304,7 @@ pub mod testing { prop_compose! { /// Generate an arbitrary account initialization transaction pub fn arb_update_account_tx()( - mut header in arb_header(), + mut header in arb_header(0), wrapper in arb_wrapper_tx(), mut update_account in arb_update_account(), extra_data in arb_code(), @@ -1325,7 +1325,7 @@ pub mod testing { prop_compose! { /// Generate an arbitrary reveal public key transaction pub fn arb_withdraw_tx()( - mut header in arb_header(), + mut header in arb_header(0), wrapper in arb_wrapper_tx(), withdraw in arb_withdraw(), code_hash in arb_hash(), @@ -1341,7 +1341,7 @@ pub mod testing { prop_compose! { /// Generate an arbitrary claim rewards transaction pub fn arb_claim_rewards_tx()( - mut header in arb_header(), + mut header in arb_header(0), wrapper in arb_wrapper_tx(), claim_rewards in arb_withdraw(), code_hash in arb_hash(), @@ -1357,7 +1357,7 @@ pub mod testing { prop_compose! { /// Generate an arbitrary commission change transaction pub fn arb_commission_change_tx()( - mut header in arb_header(), + mut header in arb_header(0), wrapper in arb_wrapper_tx(), commission_change in arb_commission_change(), code_hash in arb_hash(), @@ -1373,7 +1373,7 @@ pub mod testing { prop_compose! { /// Generate an arbitrary commission change transaction pub fn arb_metadata_change_tx()( - mut header in arb_header(), + mut header in arb_header(0), wrapper in arb_wrapper_tx(), metadata_change in arb_metadata_change(), code_hash in arb_hash(), @@ -1389,7 +1389,7 @@ pub mod testing { prop_compose! { /// Generate an arbitrary unjail validator transaction pub fn arb_unjail_validator_tx()( - mut header in arb_header(), + mut header in arb_header(0), wrapper in arb_wrapper_tx(), address in arb_non_internal_address(), code_hash in arb_hash(), @@ -1405,7 +1405,7 @@ pub mod testing { prop_compose! { /// Generate an arbitrary deactivate validator transaction pub fn arb_deactivate_validator_tx()( - mut header in arb_header(), + mut header in arb_header(0), wrapper in arb_wrapper_tx(), address in arb_non_internal_address(), code_hash in arb_hash(), @@ -1421,7 +1421,7 @@ pub mod testing { prop_compose! { /// Generate an arbitrary reactivate validator transaction pub fn arb_reactivate_validator_tx()( - mut header in arb_header(), + mut header in arb_header(0), wrapper in arb_wrapper_tx(), address in arb_non_internal_address(), code_hash in arb_hash(), @@ -1437,7 +1437,7 @@ pub mod testing { prop_compose! { /// Generate an arbitrary consensus key change transaction pub fn arb_consensus_key_change_tx()( - mut header in arb_header(), + mut header in arb_header(0), wrapper in arb_wrapper_tx(), consensus_key_change in arb_consensus_key_change(), code_hash in arb_hash(), @@ -1453,7 +1453,7 @@ pub mod testing { prop_compose! { /// Generate an arbitrary redelegation transaction pub fn arb_redelegation_tx()( - mut header in arb_header(), + mut header in arb_header(0), wrapper in arb_wrapper_tx(), redelegation in arb_redelegation(), code_hash in arb_hash(), @@ -1469,7 +1469,7 @@ pub mod testing { prop_compose! { /// Generate an arbitrary redelegation transaction pub fn arb_update_steward_commission_tx()( - mut header in arb_header(), + mut header in arb_header(0), wrapper in arb_wrapper_tx(), update_steward_commission in arb_update_steward_commission(), code_hash in arb_hash(), @@ -1485,7 +1485,7 @@ pub mod testing { prop_compose! { /// Generate an arbitrary redelegation transaction pub fn arb_resign_steward_tx()( - mut header in arb_header(), + mut header in arb_header(0), wrapper in arb_wrapper_tx(), steward in arb_non_internal_address(), code_hash in arb_hash(), @@ -1501,7 +1501,7 @@ pub mod testing { prop_compose! { /// Generate an arbitrary pending transfer transaction pub fn arb_pending_transfer_tx()( - mut header in arb_header(), + mut header in arb_header(0), wrapper in arb_wrapper_tx(), pending_transfer in arb_pending_transfer(), code_hash in arb_hash(), @@ -1534,7 +1534,7 @@ pub mod testing { prop_compose! { /// Generate an arbitrary IBC any transaction pub fn arb_ibc_msg_transfer_tx()( - mut header in arb_header(), + mut header in arb_header(0), wrapper in arb_wrapper_tx(), (msg_transfer, aux) in arb_msg_transfer(), code_hash in arb_hash(), @@ -1584,7 +1584,7 @@ pub mod testing { prop_compose! { /// Generate an arbitrary IBC any transaction pub fn arb_ibc_msg_nft_transfer_tx()( - mut header in arb_header(), + mut header in arb_header(0), wrapper in arb_wrapper_tx(), (msg_transfer, aux) in arb_msg_nft_transfer(), code_hash in arb_hash(), diff --git a/crates/sdk/src/signing.rs b/crates/sdk/src/signing.rs index 98d87332ec..a43b432968 100644 --- a/crates/sdk/src/signing.rs +++ b/crates/sdk/src/signing.rs @@ -21,11 +21,15 @@ use namada_core::key::*; use namada_core::masp::{ AssetData, ExtendedViewingKey, MaspTxId, PaymentAddress, }; +use namada_core::time::DateTimeUtc; use namada_core::token::{Amount, DenominatedAmount}; use namada_governance::storage::proposal::{ InitProposalData, ProposalType, VoteProposalData, }; use namada_governance::storage::vote::ProposalVote; +use namada_ibc::core::channel::types::timeout::{ + TimeoutHeight, TimeoutTimestamp, +}; use namada_ibc::{MsgNftTransfer, MsgTransfer}; use namada_io::*; use namada_parameters::storage as parameter_storage; @@ -1002,6 +1006,44 @@ fn find_masp_builder<'a>( Ok(None) } +// Format the date-time for the Ledger device +fn format_timestamp(datetime: DateTimeUtc) -> String { + let mut datetime = datetime.0.to_string(); + let mut secfrac_width = None; + for (i, ch) in datetime.char_indices() { + if ch == '.' { + secfrac_width = Some(0); + } else if let Some(ref mut secfrac_width) = &mut secfrac_width { + if ch.is_ascii_digit() { + *secfrac_width += 1; + } else { + let trailing = "0".repeat(9 - *secfrac_width); + datetime.insert_str(i, &trailing); + break; + } + } + } + datetime +} + +// Format the timeout timestamp for the Ledger device +fn format_timeout_timestamp(timestamp: &TimeoutTimestamp) -> String { + match timestamp { + TimeoutTimestamp::Never => "no timestamp".to_string(), + TimeoutTimestamp::At(timestamp) => { + timestamp.into_tm_time().to_rfc3339() + } + } +} + +// Format the timeout height for the Ledger device +fn format_timeout_height(height: &TimeoutHeight) -> String { + match height { + TimeoutHeight::Never => "no timeout".to_string(), + TimeoutHeight::At(height) => height.to_string(), + } +} + /// Converts the given transaction to the form that is displayed on the Ledger /// device pub async fn to_ledger_vector( @@ -1391,11 +1433,15 @@ pub async fn to_ledger_vector( ), format!( "Timeout height : {}", - transfer.message.timeout_height_on_b + format_timeout_height( + &transfer.message.timeout_height_on_b + ) ), format!( "Timeout timestamp : {}", - transfer.message.timeout_timestamp_on_b, + format_timeout_timestamp( + &transfer.message.timeout_timestamp_on_b + ), ), ]); tv.output_expert.extend(vec![ @@ -1420,11 +1466,15 @@ pub async fn to_ledger_vector( tv.output_expert.extend(vec![ format!( "Timeout height : {}", - transfer.message.timeout_height_on_b + format_timeout_height( + &transfer.message.timeout_height_on_b + ) ), format!( "Timeout timestamp : {}", - transfer.message.timeout_timestamp_on_b, + format_timeout_timestamp( + &transfer.message.timeout_timestamp_on_b + ), ), ]); if let Some(transfer) = transfer.transfer { @@ -1513,11 +1563,15 @@ pub async fn to_ledger_vector( tv.output.extend(vec![ format!( "Timeout height : {}", - transfer.message.timeout_height_on_b + format_timeout_height( + &transfer.message.timeout_height_on_b + ) ), format!( "Timeout timestamp : {}", - transfer.message.timeout_timestamp_on_b, + format_timeout_timestamp( + &transfer.message.timeout_timestamp_on_b + ), ), ]); tv.output_expert.extend(vec![ @@ -1581,11 +1635,15 @@ pub async fn to_ledger_vector( tv.output_expert.extend(vec![ format!( "Timeout height : {}", - transfer.message.timeout_height_on_b + format_timeout_height( + &transfer.message.timeout_height_on_b + ) ), format!( "Timeout timestamp : {}", - transfer.message.timeout_timestamp_on_b, + format_timeout_timestamp( + &transfer.message.timeout_timestamp_on_b + ), ), ]); if let Some(transfer) = transfer.transfer { @@ -2022,7 +2080,10 @@ pub async fn to_ledger_vector( let fee_amount_per_gas_unit = to_ledger_decimal(&wrapper.fee.amount_per_gas_unit.to_string()); tv.output_expert.extend(vec![ - format!("Timestamp : {}", tx.header.timestamp.0), + format!( + "Timestamp : {}", + format_timestamp(tx.header.timestamp) + ), format!("Pubkey : {}", wrapper.pk), format!("Gas limit : {}", u64::from(wrapper.gas_limit)), ]);