commit a9326b1c867940da4dba77c81ff81ba52630a113
parent 2c6788738c5f4784874e62c91e32dab4550c4626
Author: Antoine A <>
Date: Tue, 29 Sep 2026 17:55:27 +0200
common: stricter prepared transfer tests and fixes
Diffstat:
7 files changed, 96 insertions(+), 18 deletions(-)
diff --git a/adapters/taler-cyclos/db/cyclos-procedures.sql b/adapters/taler-cyclos/db/cyclos-procedures.sql
@@ -129,7 +129,7 @@ ELSIF in_type IS NOT NULL THEN
UPDATE prepared_in
SET tx_in_id = out_tx_row_id
WHERE (
- tx_in_id IS NULL AND account_pub = in_metadata AND type='reserve'
+ tx_in_id IS NULL AND account_pub = in_metadata AND in_type=type AND type='reserve'
) OR authorization_pub = local_authorization_pub;
-- Insert new incoming talerable transaction
INSERT INTO taler_in (
@@ -439,7 +439,7 @@ WHERE authorization_pub = in_authorization_pub;
-- Check idempotency and delay garbage collection
IF FOUND AND idempotent THEN
UPDATE prepared_in
- SET registered_at=in_timestamp
+ SET registered_at=in_timestamp,authorization_sig=in_authorization_sig
WHERE authorization_pub=in_authorization_pub;
RETURN;
END IF;
diff --git a/adapters/taler-cyclos/src/api.rs b/adapters/taler-cyclos/src/api.rs
@@ -459,13 +459,10 @@ mod test {
.await
.assert_ok_json::<RevenueConfig>();
server
- .get(format!("/taler-observability/config"))
+ .get("/taler-observability/config")
.await
.assert_ok_json::<Config>();
- server
- .get(format!("/taler-observability/metrics"))
- .await
- .assert_ok();
+ server.get("/taler-observability/metrics").await.assert_ok();
}
#[tokio::test]
diff --git a/adapters/taler-magnet-bank/db/magnet-bank-procedures.sql b/adapters/taler-magnet-bank/db/magnet-bank-procedures.sql
@@ -125,7 +125,7 @@ ELSIF in_type IS NOT NULL THEN
UPDATE prepared_in
SET tx_in_id = out_tx_row_id
WHERE (
- tx_in_id IS NULL AND account_pub = in_metadata AND type='reserve'
+ tx_in_id IS NULL AND account_pub = in_metadata AND in_type=type AND type='reserve'
) OR authorization_pub = local_authorization_pub;
-- Insert new incoming talerable transaction
INSERT INTO taler_in (
@@ -491,7 +491,7 @@ WHERE authorization_pub = in_authorization_pub;
-- Check idempotency and delay garbage collection
IF FOUND AND idempotent THEN
UPDATE prepared_in
- SET registered_at=in_timestamp
+ SET registered_at=in_timestamp,authorization_sig=in_authorization_sig
WHERE authorization_pub=in_authorization_pub;
RETURN;
END IF;
diff --git a/adapters/taler-magnet-bank/src/api.rs b/adapters/taler-magnet-bank/src/api.rs
@@ -427,13 +427,10 @@ mod test {
.await
.assert_ok_json::<RevenueConfig>();
server
- .get(format!("/taler-observability/config"))
+ .get("/taler-observability/config")
.await
.assert_ok_json::<Config>();
- server
- .get(format!("/taler-observability/metrics"))
- .await
- .assert_ok();
+ server.get("/taler-observability/metrics").await.assert_ok();
}
#[tokio::test]
diff --git a/adapters/taler-wise/db/wise-procedures.sql b/adapters/taler-wise/db/wise-procedures.sql
@@ -130,7 +130,7 @@ ELSIF in_type IS NOT NULL AND in_debit_payto IS NOT NULL THEN
UPDATE prepared_in
SET tx_in_id = out_tx_row_id
WHERE (
- tx_in_id IS NULL AND account_pub = in_metadata AND type='reserve'
+ tx_in_id IS NULL AND account_pub = in_metadata AND in_type=type AND type='reserve'
) OR authorization_pub = local_authorization_pub;
-- Insert new incoming talerable transaction
INSERT INTO taler_in (
@@ -181,7 +181,7 @@ WHERE authorization_pub = in_authorization_pub;
-- Check idempotency and delay garbage collection
IF FOUND AND idempotent THEN
UPDATE prepared_in
- SET registered_at=in_timestamp
+ SET registered_at=in_timestamp,authorization_sig=in_authorization_sig
WHERE authorization_pub=in_authorization_pub;
RETURN;
END IF;
diff --git a/common/taler-api/db/taler-api-procedures.sql b/common/taler-api/db/taler-api-procedures.sql
@@ -176,7 +176,7 @@ ELSE
UPDATE prepared_in
SET tx_in_id = out_tx_row_id
WHERE (
- tx_in_id IS NULL AND account_pub = in_account_pub AND type='reserve'
+ tx_in_id IS NULL AND account_pub = in_account_pub AND in_type=type AND type='reserve'
) OR authorization_pub = local_authorization_pub;
INSERT INTO taler_in (
tx_in_id,
@@ -225,7 +225,7 @@ WHERE authorization_pub = in_authorization_pub;
-- Check idempotency and delay garbage collection
IF FOUND AND idempotent THEN
UPDATE prepared_in
- SET registered_at=in_timestamp
+ SET registered_at=in_timestamp,authorization_sig=in_authorization_sig
WHERE authorization_pub=in_authorization_pub;
RETURN;
END IF;
diff --git a/common/taler-test-utils/src/routine.rs b/common/taler-test-utils/src/routine.rs
@@ -43,6 +43,7 @@ use taler_common::{
},
db::IncomingType,
error_code::ErrorCode,
+ signature::Signature as _,
types::{
amount::{Amount, Currency, amount},
base32::Base32,
@@ -1621,6 +1622,86 @@ pub async fn registration_routine<F1: Future<Output = Vec<Status>>>(
.await
.assert_error(ErrorCode::BANK_OLD_TIMESTAMP);
+ // A KYC payment with a reserve's account key must not consume that
+ // reserve's prepared registration. The mapped reserve is still valid.
+ let reserve_pair = Ed25519KeyPair::generate().unwrap();
+ let authorization_pub = EddsaPublicKey::try_from(reserve_pair.public_key().as_ref()).unwrap();
+ let reserve_pub = EddsaPublicKey::rand();
+ prepared_transfer
+ .post("/registration")
+ .json(
+ RegistrationRequest {
+ account_pub: reserve_pub,
+ authorization_pub,
+ ..req.clone()
+ }
+ .signed(&reserve_pair),
+ )
+ .await
+ .assert_ok_json::<RegistrationResponse>();
+ wire_gateway
+ .post("/admin/add-kycauth")
+ .json(json!({
+ "amount": amount,
+ "account_pub": reserve_pub,
+ "debit_account": debit_acount,
+ }))
+ .await
+ .assert_ok_json::<TransferResponse>();
+ register(&authorization_pub)
+ .await
+ .assert_ok_json::<TransferResponse>();
+
+ // Re-registering the same key with changed signed information must
+ // replace the signature retained for the next mapped transfer.
+ let replacement_pair = Ed25519KeyPair::generate().unwrap();
+ let replacement_auth_pub =
+ EddsaPublicKey::try_from(replacement_pair.public_key().as_ref()).unwrap();
+ let replacement_reserve_pub = EddsaPublicKey::rand();
+ let original_registration = RegistrationRequest {
+ account_pub: replacement_reserve_pub,
+ authorization_pub: replacement_auth_pub,
+ ..req.clone()
+ }
+ .signed(&replacement_pair);
+ prepared_transfer
+ .post("/registration")
+ .json(&original_registration)
+ .await
+ .assert_ok_json::<RegistrationResponse>();
+ let replacement_registration = RegistrationRequest {
+ credit_amount: taler_common::types::amount::amount(format!("{currency}:43")),
+ ..original_registration
+ }
+ .signed(&replacement_pair);
+ prepared_transfer
+ .post("/registration")
+ .json(&replacement_registration)
+ .await
+ .assert_ok_json::<RegistrationResponse>();
+ let before_replacement_transfer =
+ latest_id::<IncomingHistory>(&wire_gateway.suffix("/history/incoming")).await;
+ register(&replacement_auth_pub)
+ .await
+ .assert_ok_json::<TransferResponse>();
+ let latest = wire_gateway
+ .get(format!(
+ "/history/incoming?limit=1&offset={before_replacement_transfer}"
+ ))
+ .await
+ .assert_ok_json::<IncomingHistory>();
+ assert!(matches!(
+ &latest.incoming_transactions[0],
+ IncomingBankTransaction::Reserve {
+ reserve_pub,
+ authorization_pub: Some(auth_pub),
+ authorization_sig: Some(auth_sig),
+ ..
+ } if *reserve_pub == replacement_reserve_pub
+ && *auth_pub == replacement_auth_pub
+ && replacement_registration.verify(auth_pub, auth_sig)
+ ));
+
/* ----- API ----- */
let history: Vec<_> = wire_gateway
@@ -1662,6 +1743,9 @@ pub async fn registration_routine<F1: Future<Output = Vec<Status>>>(
(acc_pub5, None),
(acc_pub5, None),
(acc_pub5, Some(auth_pub2)),
+ (reserve_pub, None),
+ (reserve_pub, Some(authorization_pub)),
+ (replacement_reserve_pub, Some(replacement_auth_pub)),
]
)
}