Skip to content

Commit df98f63

Browse files
committed
Add ledger CI tests
1 parent b66c972 commit df98f63

File tree

5 files changed

+117
-55
lines changed

5 files changed

+117
-55
lines changed

.github/workflows/build.yml

Lines changed: 91 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,14 @@ jobs:
2828
- name: Install rust
2929
run: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain $(python3 ./build-tools/cargo-info-extractor/extract.py --rust-version)
3030
- name: Build
31-
run: cargo build --release --locked --features trezor
31+
run: cargo build --release --locked --features trezor,ledger
3232
- name: Run tests
33-
run: cargo test --release --workspace --features trezor
33+
run: cargo test --release --workspace --features trezor,ledger
3434
- name: Run doc tests
35-
run: cargo test --release --doc --features trezor
35+
run: cargo test --release --doc --features trezor,ledger
3636
# This test is ignored, so it needs to run separately.
3737
- name: Run mixed_sighash_types test
38-
run: cargo test --release mixed_sighash_types --features trezor
38+
run: cargo test --release mixed_sighash_types --features trezor,ledger
3939
# This test is ignored, so it needs to run separately.
4040
- name: Run test_4opc_sequences test
4141
run: cargo test --release test_4opc_sequences -- --ignored
@@ -63,14 +63,14 @@ jobs:
6363
- name: Install rust
6464
run: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain $(python3 ./build-tools/cargo-info-extractor/extract.py --rust-version)
6565
- name: Build
66-
run: cargo build --release --locked --features trezor
66+
run: cargo build --release --locked --features trezor,ledger
6767
- name: Run tests
68-
run: cargo test --release --workspace --features trezor
68+
run: cargo test --release --workspace --features trezor,ledger
6969
- name: Run doc tests
70-
run: cargo test --release --doc --features trezor
70+
run: cargo test --release --doc --features trezor,ledger
7171
# This test is ignored, so it needs to run separately.
7272
- name: Run mixed_sighash_types test
73-
run: cargo test --release mixed_sighash_types --features trezor
73+
run: cargo test --release mixed_sighash_types --features trezor,ledger
7474
# This test is ignored, so it needs to run separately.
7575
- name: Run test_4opc_sequences test
7676
run: cargo test --release test_4opc_sequences
@@ -94,14 +94,14 @@ jobs:
9494
- name: Install rust
9595
run: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain $(python3 ./build-tools/cargo-info-extractor/extract.py --rust-version)
9696
- name: Build
97-
run: cargo build --release --locked --features trezor
97+
run: cargo build --release --locked --features trezor,ledger
9898
- name: Run tests
99-
run: cargo test --release --workspace --features trezor
99+
run: cargo test --release --workspace --features trezor,ledger
100100
- name: Run doc tests
101-
run: cargo test --release --doc --features trezor
101+
run: cargo test --release --doc --features trezor,ledger
102102
# This test is ignored, so it needs to run separately.
103103
- name: Run mixed_sighash_types test
104-
run: cargo test --release mixed_sighash_types --features trezor
104+
run: cargo test --release mixed_sighash_types --features trezor,ledger
105105
# This test is ignored, so it needs to run separately.
106106
- name: Run test_4opc_sequences test
107107
run: cargo test --release test_4opc_sequences
@@ -236,3 +236,82 @@ jobs:
236236
"
237237
working-directory: ./mintlayer-trezor-firmware
238238
timeout-minutes: 10
239+
240+
# Build Ledger-specific tests and archive them
241+
run_tests_on_ledger_preparation:
242+
runs-on: ubuntu-latest
243+
steps:
244+
- name: Checkout the core repository
245+
uses: actions/checkout@v4
246+
with:
247+
submodules: recursive
248+
path: ./mintlayer-core
249+
- name: Update local dependency repositories
250+
run: sudo apt-get update
251+
- name: Install build dependencies
252+
run: sudo apt-get install -yqq --no-install-recommends build-essential pkg-config libdbus-1-dev libusb-1.0-0-dev
253+
- name: Install rust
254+
run: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
255+
- name: Install cargo-nextest
256+
uses: taiki-e/install-action@nextest
257+
- name: Build and archive the tests
258+
run: cargo nextest archive --release --locked -p wallet --features enable-ledger-device-tests --archive-file ledger-tests.tar.zst
259+
working-directory: ./mintlayer-core
260+
- name: Upload archived tests
261+
uses: actions/upload-artifact@v4
262+
with:
263+
name: archived-ledger-tests
264+
path: ./mintlayer-core/ledger-tests.tar.zst
265+
retention-days: 1
266+
267+
# Run Ledger-specific tests on an emulator
268+
run_tests_on_ledger:
269+
needs: run_tests_on_ledger_preparation
270+
runs-on: ubuntu-latest
271+
steps:
272+
- name: Checkout the core repository
273+
uses: actions/checkout@v4
274+
with:
275+
submodules: recursive
276+
path: ./mintlayer-core
277+
- name: Checkout mintlayer-ledger-app repository
278+
uses: actions/checkout@v4
279+
with:
280+
repository: mintlayer/mintlayer-ledger-app
281+
ref: feature/mintlayer-app
282+
path: ./mintlayer-ledger-app
283+
- name: Download archived tests
284+
uses: actions/download-artifact@v4
285+
with:
286+
name: archived-ledger-tests
287+
path: ./mintlayer-core
288+
- name: Install cargo-nextest
289+
uses: taiki-e/install-action@nextest
290+
- name: Build Ledger app in container
291+
run: |
292+
sudo docker run --rm \
293+
-v "$(realpath ./mintlayer-ledger-app):/app" \
294+
ghcr.io/ledgerhq/ledger-app-builder/ledger-app-dev-tools:latest \
295+
sh -c 'cd /app && cargo ledger build nanosplus'
296+
- name: Run Ledger emulator and execute tests
297+
run: |
298+
set -e
299+
300+
sudo docker run -d --rm --name ledger-emulator \
301+
-v "$(realpath ./mintlayer-ledger-app):/app" \
302+
--publish 5001:5001 --publish 9999:9999 \
303+
ghcr.io/ledgerhq/ledger-app-builder/ledger-app-dev-tools:latest \
304+
sh -c 'cd /app && speculos --apdu-port 9999 --api-port 5001 --display headless --model nanosp -s "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" target/nanosplus/release/mintlayer-app'
305+
306+
echo "--- Waiting for emulator to initialize ---"
307+
sleep 15
308+
309+
# Set up a trap to ensure the container is stopped even if tests fail or the job is cancelled
310+
trap "echo '--- Dumping Ledger emulator logs ---'; sudo docker logs ledger-emulator; echo '--- Stopping Ledger emulator ---'; sudo docker stop ledger-emulator" EXIT
311+
312+
echo "--- Running Ledger device tests on the host ---"
313+
cd ./mintlayer-core
314+
cargo-nextest nextest run --archive-file ledger-tests.tar.zst -j1 ledger_signer || test_exit_code=$?
315+
316+
exit $test_exit_code
317+
timeout-minutes: 15

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,7 @@ rev = "510bb3ca30639af4bdb12a918b6bbbdb75fa5f52"
273273
[workspace.dependencies.mintlayer-ledger-messages]
274274
git = "https://github.com/mintlayer/mintlayer-ledger-app"
275275
# The commit "Fix comments"
276-
rev = "2fa1c33e536f94ba3cac94b97054902674580686"
276+
rev = "78bd2e2514be3a6db83e7493eaaa03c40acc5409"
277277
package = "messages"
278278

279279
[workspace.dependencies.trezor-client]

wallet/src/signer/ledger_signer/ledger_messages.rs

Lines changed: 21 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -22,36 +22,28 @@ use common::{
2222
};
2323
use crypto::key::{
2424
extended::ExtendedPublicKey,
25-
hdkd::{
26-
chain_code::{ChainCode, CHAINCODE_LENGTH},
27-
derivation_path::DerivationPath,
28-
},
25+
hdkd::{chain_code::ChainCode, derivation_path::DerivationPath},
2926
secp256k1::{extended_keys::Secp256k1ExtendedPublicKey, Secp256k1PublicKey},
3027
};
31-
use serialization::{Decode, DecodeAll, Encode};
28+
use serialization::Encode;
3229
use utils::ensure;
3330
use wallet_types::hw_data::LedgerFullInfo;
3431

3532
use ledger_lib::Exchange;
3633
use mintlayer_ledger_messages::{
3734
decode_all as ledger_decode_all, encode as ledger_encode, AddrType, Amount as LAmount,
38-
Bip32Path as LedgerBip32Path, CoinType, InputAdditionalInfoReq, Ins,
39-
OutputValue as LOutputValue, P1SignTx, PubKeyP1, PublicKeyReq, SignMessageReq, SignTxReq,
40-
TxInput as LTxInput, TxInputReq, TxMetadataReq, TxOutput as LTxOutput, TxOutputReq, APDU_CLASS,
41-
H256 as LH256, P1_APP_NAME, P1_GET_VERSION, P1_SIGN_NEXT, P1_SIGN_START, P2_DONE, P2_SIGN_MORE,
35+
Bip32Path as LedgerBip32Path, CoinType, GetPublicKeyRespones, GetVersionRespones,
36+
InputAdditionalInfoReq, Ins, MsgSignature, OutputValue as LOutputValue, P1SignTx, PubKeyP1,
37+
PublicKeyReq, SignMessageReq, SignTxReq, Signature as LedgerSignature, TxInput as LTxInput,
38+
TxInputReq, TxMetadataReq, TxOutput as LTxOutput, TxOutputReq, APDU_CLASS, H256 as LH256,
39+
P1_APP_NAME, P1_GET_VERSION, P1_SIGN_NEXT, P1_SIGN_START, P2_DONE, P2_SIGN_MORE,
4240
};
4341

4442
const MAX_ADPU_LEN: usize = (u8::MAX - 5) as usize; // 4 bytes for the header + 1 for len
4543
const TIMEOUT_DUR: Duration = Duration::from_secs(100);
4644
const OK_RESPONSE: u16 = 0x9000;
4745
const TX_VERSION: u8 = 1;
4846

49-
#[derive(Decode)]
50-
pub struct LedgerSignature {
51-
pub signature: [u8; 64],
52-
pub multisig_idx: Option<u32>,
53-
}
54-
5547
struct SignatureResult {
5648
sig: LedgerSignature,
5749
input_idx: usize,
@@ -129,10 +121,9 @@ pub async fn sign_challenge<L: Exchange>(
129121

130122
let resp = send_chunked(ledger, Ins::SIGN_MSG, P1_SIGN_NEXT, message).await?;
131123

132-
let sig_len = *resp.first().ok_or(LedgerError::InvalidResponse)? as usize;
133-
let sig = resp.as_slice().get(1..1 + sig_len).ok_or(LedgerError::InvalidResponse)?;
124+
let sig: MsgSignature = ledger_decode_all(&resp).ok_or(LedgerError::InvalidResponse)?;
134125

135-
Ok(sig.to_vec())
126+
Ok(sig.signature.to_vec())
136127
}
137128

138129
pub async fn get_app_name<L: Exchange>(ledger: &mut L) -> Result<Vec<u8>, ledger_lib::Error> {
@@ -161,13 +152,12 @@ pub async fn check_current_app<L: Exchange>(ledger: &mut L) -> SignerResult<Ledg
161152
.await
162153
.map_err(|err| LedgerError::DeviceError(err.to_string()))?;
163154
let ver = ok_response(resp)?;
164-
let app_version = match ver.as_slice() {
165-
[major, minor, patch] => common::primitives::semver::SemVer {
166-
major: *major,
167-
minor: *minor,
168-
patch: *patch as u16,
169-
},
170-
_ => return Err(SignerError::LedgerError(LedgerError::InvalidResponse)),
155+
let app_version_resp: GetVersionRespones =
156+
ledger_decode_all(&ver).ok_or(LedgerError::InvalidResponse)?;
157+
let app_version = common::primitives::semver::SemVer {
158+
major: app_version_resp.major,
159+
minor: app_version_resp.minor,
160+
patch: app_version_resp.patch as u16,
171161
};
172162

173163
Ok(LedgerFullInfo { app_version })
@@ -191,20 +181,13 @@ pub async fn get_extended_public_key<L: Exchange>(
191181
)
192182
.await?;
193183

194-
let pk_len = *resp.first().ok_or(LedgerError::InvalidResponse)? as usize;
195-
let public_key = resp.as_slice().get(1..1 + pk_len).ok_or(LedgerError::InvalidResponse)?;
196-
let chain_code_len = *resp.get(1 + pk_len).ok_or(LedgerError::InvalidResponse)? as usize;
197-
let chain_code: [_; CHAINCODE_LENGTH] = resp
198-
.as_slice()
199-
.get(2 + pk_len..2 + pk_len + chain_code_len)
200-
.ok_or(LedgerError::InvalidResponse)?
201-
.try_into()
202-
.map_err(|_| LedgerError::InvalidKey)?;
184+
let resp: GetPublicKeyRespones =
185+
ledger_decode_all(&resp).ok_or(LedgerError::InvalidResponse)?;
203186

204187
let extended_public_key = Secp256k1ExtendedPublicKey::new_unchecked(
205188
derivation_path,
206-
ChainCode::from(chain_code),
207-
Secp256k1PublicKey::from_bytes(public_key).map_err(|_| LedgerError::InvalidKey)?,
189+
ChainCode::from(resp.chain_code),
190+
Secp256k1PublicKey::from_bytes(&resp.public_key).map_err(|_| LedgerError::InvalidKey)?,
208191
);
209192

210193
Ok(ExtendedPublicKey::new(extended_public_key))
@@ -286,8 +269,8 @@ fn decode_signature_response(resp: &[u8]) -> Result<SignatureResult, LedgerError
286269
let input_idx = *resp.first().ok_or(LedgerError::InvalidResponse)? as usize;
287270
let has_more_signatures = *resp.last().ok_or(LedgerError::InvalidResponse)? == P2_SIGN_MORE;
288271

289-
let sig = LedgerSignature::decode_all(&mut &resp[..resp.len() - 1][1..])
290-
.map_err(|_| LedgerError::InvalidResponse)?;
272+
let sig: LedgerSignature =
273+
ledger_decode_all(&resp[..resp.len() - 1][1..]).ok_or(LedgerError::InvalidResponse)?;
291274

292275
Ok(SignatureResult {
293276
sig,

wallet/src/signer/ledger_signer/mod.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ use crate::{
2323
ledger_signer::ledger_messages::{
2424
check_current_app, get_app_name, get_extended_public_key, sign_challenge, sign_tx,
2525
to_ledger_amount, to_ledger_output_value, to_ledger_tx_input, to_ledger_tx_output,
26-
LedgerSignature,
2726
},
2827
utils::{is_htlc_utxo, produce_uniparty_signature_for_input},
2928
Signer, SignerError, SignerProvider, SignerResult,
@@ -81,7 +80,8 @@ use itertools::{izip, Itertools};
8180
use ledger_lib::{Exchange, Filters, LedgerHandle, LedgerProvider, Transport};
8281
use mintlayer_ledger_messages::{
8382
AddrType, Bip32Path as LedgerBip32Path, CoinType, InputAdditionalInfoReq,
84-
InputAddressPath as LedgerInputAddressPath, TxInputReq, TxOutputReq,
83+
InputAddressPath as LedgerInputAddressPath, Signature as LedgerSignature, TxInputReq,
84+
TxOutputReq,
8585
};
8686
use randomness::make_true_rng;
8787
use tokio::sync::Mutex;
@@ -669,8 +669,8 @@ where
669669
(Some(destination), None) => {
670670
let standalone = match standalone_inputs.get(&(input_index as u32)).map(|x| x.as_slice()) {
671671
Some([standalone]) => standalone,
672+
Some([]) | None => return Ok((None, SignatureStatus::NotSigned, SignatureStatus::NotSigned)),
672673
Some(_) => return Err(LedgerError::MultisigSignatureReturned.into()),
673-
None => return Ok((None, SignatureStatus::NotSigned, SignatureStatus::NotSigned))
674674
};
675675

676676
let sig = produce_uniparty_signature_for_input(

0 commit comments

Comments
 (0)