use dig_pex::{
Address, PaymentClaim, PaymentClaimError, PeerEntry, Provenance, SignatureVerifier, ValidateCtx,
};
use p256::ecdsa::signature::{Signer, Verifier};
use p256::ecdsa::{Signature, SigningKey, VerifyingKey};
use p256::pkcs8::{DecodePublicKey, EncodePublicKey};
struct P256Verifier;
impl SignatureVerifier for P256Verifier {
fn verify(&self, spki_der: &[u8], message: &[u8], signature: &[u8]) -> bool {
let Ok(vk) = VerifyingKey::from_public_key_der(spki_der) else {
return false;
};
let Ok(sig) = Signature::from_der(signature) else {
return false;
};
vk.verify(message, &sig).is_ok()
}
}
struct Node {
signing_key: SigningKey,
spki_der: Vec<u8>,
}
impl Node {
fn new(seed: u8) -> Self {
let signing_key = SigningKey::from_bytes(&[seed.max(1); 32].into())
.expect("a non-zero 32-byte scalar is a valid P-256 signing key");
let spki_der = VerifyingKey::from(&signing_key)
.to_public_key_der()
.expect("P-256 public keys encode to SPKI DER")
.as_bytes()
.to_vec();
Node {
signing_key,
spki_der,
}
}
fn peer_id(&self) -> String {
dig_pex::peer_id_for_spki(&self.spki_der)
}
fn claim_signed_for(&self, peer_id: &str, network_id: &str, address: &str) -> PaymentClaim {
let msg = dig_pex::payment_signing_bytes(peer_id, network_id, address);
let sig: Signature = self.signing_key.sign(&msg);
PaymentClaim::new(address, &self.spki_der, sig.to_der().as_bytes())
}
fn claim(&self, network_id: &str, address: &str) -> PaymentClaim {
self.claim_signed_for(&self.peer_id(), network_id, address)
}
}
const MAINNET: &str = "mainnet";
const TESTNET: &str = "testnet11";
const PAYEE: &str = "xch1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqs7lyw6y";
fn entry_for(node: &Node, network_id: &str, claim: Option<PaymentClaim>) -> PeerEntry {
let mut e = PeerEntry::new(node.peer_id(), network_id, 1_000, Provenance::Direct)
.with_address(Address::direct("203.0.113.7", 9444));
if let Some(c) = claim {
e = e.with_payment(c);
}
e
}
#[test]
fn honest_claim_yields_the_payment_address() {
let node = Node::new(7);
let entry = entry_for(&node, MAINNET, Some(node.claim(MAINNET, PAYEE)));
assert_eq!(
entry.verified_payment_address(&P256Verifier),
Ok(PAYEE),
"a first-party signature over (peer_id, network_id, address) must yield the payee"
);
}
#[test]
fn a_relaying_attacker_cannot_substitute_its_own_payee() {
let victim = Node::new(7);
let attacker = Node::new(9);
let attacker_payee = "xch1attackerattackerattackerattackerattackerattackerattackerattacke";
let entry = entry_for(&victim, MAINNET, None).with_payment(attacker.claim_signed_for(
&victim.peer_id(),
MAINNET,
attacker_payee,
));
assert_eq!(
entry.verified_payment_address(&P256Verifier),
Err(PaymentClaimError::PeerIdMismatch),
"a signature by a key that does not hash to peer_id must never name a payee"
);
}
#[test]
fn a_valid_claim_lifted_onto_another_peer_is_refused() {
let earner = Node::new(7);
let other = Node::new(11);
let entry = entry_for(&other, MAINNET, None).with_payment(earner.claim(MAINNET, PAYEE));
assert_eq!(
entry.verified_payment_address(&P256Verifier),
Err(PaymentClaimError::PeerIdMismatch),
);
}
#[test]
fn a_cross_network_replay_is_refused() {
let node = Node::new(7);
let replayed = entry_for(&node, MAINNET, None).with_payment(node.claim(TESTNET, PAYEE));
assert_eq!(
replayed.verified_payment_address(&P256Verifier),
Err(PaymentClaimError::BadSignature),
"a claim signed for another network must not be honoured on this one"
);
let at_home = entry_for(&node, TESTNET, None).with_payment(node.claim(TESTNET, PAYEE));
assert_eq!(at_home.verified_payment_address(&P256Verifier), Ok(PAYEE));
}
#[test]
fn a_rewritten_address_is_refused() {
let node = Node::new(7);
let honest = node.claim(MAINNET, PAYEE);
let tampered = PaymentClaim::new(
"xch1thiefthiefthiefthiefthiefthiefthiefthiefthiefthiefthiefthiefth",
&honest.spki_der(),
&honest.signature(),
);
let entry = entry_for(&node, MAINNET, None).with_payment(tampered);
assert_eq!(
entry.verified_payment_address(&P256Verifier),
Err(PaymentClaimError::BadSignature),
);
}
#[test]
fn an_unverifiable_claim_leaves_the_entry_dialable() {
let victim = Node::new(7);
let attacker = Node::new(9);
let entry = entry_for(&victim, MAINNET, None).with_payment(attacker.claim_signed_for(
&victim.peer_id(),
MAINNET,
PAYEE,
));
let receiver = "a".repeat(64);
let sender = "b".repeat(64);
let ctx = ValidateCtx {
receiver_peer_id: &receiver,
sender_peer_id: &sender,
network_id: MAINNET,
now_secs: 1_000,
};
assert_eq!(entry.validate(&ctx), Ok(()), "still a usable dial hint");
assert_eq!(entry.addresses.len(), 1);
assert!(entry.verified_payment_address(&P256Verifier).is_err());
}
#[test]
fn a_record_without_the_field_still_decodes_and_is_simply_unpayable() {
let legacy = concat!(
r#"{"peer_id":"0707070707070707070707070707070707070707070707070707070707070707","#,
r#""addresses":[{"host":"203.0.113.7","port":9444,"kind":"direct"}],"#,
r#""network_id":"mainnet","last_seen":1000,"via":"direct","flags":["storage"]}"#
);
let entry: PeerEntry = serde_json::from_str(legacy).expect("a pre-payment record must decode");
assert_eq!(entry.addresses.len(), 1);
assert_eq!(
entry.verified_payment_address(&P256Verifier),
Err(PaymentClaimError::NotPresent),
);
let json = serde_json::to_string(&entry).unwrap();
assert!(
!json.contains("payment"),
"an entry with no claim must not grow a payment key: {json}"
);
}
fn maximal_entry(node: &Node, claim: Option<PaymentClaim>) -> PeerEntry {
let mut e = PeerEntry::new(node.peer_id(), MAINNET, 1_000, Provenance::Direct);
for a in 0..dig_pex::PEX_MAX_ADDRESSES {
e = e.with_address(Address::direct(
"2001:0db8:0000:0000:0000:ff00:0042:8329",
9444 + a as u16,
));
}
for f in 0..dig_pex::PEX_MAX_FLAGS {
e = e.with_flag(format!("{}{f}", "x".repeat(dig_pex::PEX_MAX_FLAG_LEN - 1)));
}
match claim {
Some(c) => e.with_payment(c),
None => e,
}
}
#[test]
fn a_claim_costs_about_a_third_of_a_kilobyte_per_entry() {
let node = Node::new(7);
let with = serde_json::to_vec(&maximal_entry(&node, Some(node.claim(MAINNET, PAYEE)))).unwrap();
let without = serde_json::to_vec(&maximal_entry(&node, None)).unwrap();
let cost = with.len() - without.len();
assert!(
(300..=360).contains(&cost),
"the payment claim costs {cost} B/entry (P-256: 68 B address + 124 B base64 SPKI \
+ ~96 B base64 signature + JSON keys); the frame budget assumes ~334"
);
}
#[test]
fn an_engine_never_emits_a_snapshot_over_the_frame_cap() {
let node = Node::new(7);
let claim = node.claim(MAINNET, PAYEE);
let mut engine = dig_pex::PexEngine::new(
dig_pex::PexConfig::new("a".repeat(64), MAINNET).with_jitter(false),
);
for i in 0..dig_pex::PEX_MAX_SNAPSHOT {
let mut e = maximal_entry(&node, Some(claim.clone()));
e.peer_id = format!("{:064x}", i + 1);
engine.upsert_known(e);
}
let out = engine.link_up(&"b".repeat(64), 1_000_000);
let dig_pex::PexMessage::PexSnapshot { peers } = &out[1] else {
panic!("link_up emits handshake then snapshot");
};
assert!(
peers.len() < dig_pex::PEX_MAX_SNAPSHOT,
"the fixture must actually exceed the byte budget, else the trim is untested"
);
assert!(
dig_pex::caps::frame_within_bound(out[1].encode().len()),
"emitted snapshot frame is {} B, over the {} B cap",
out[1].encode().len(),
dig_pex::PEX_MAX_FRAME
);
}
#[test]
fn a_claim_verifies_after_a_json_round_trip() {
let node = Node::new(7);
let entry = entry_for(&node, MAINNET, Some(node.claim(MAINNET, PAYEE)));
let decoded: PeerEntry = serde_json::from_str(&serde_json::to_string(&entry).unwrap()).unwrap();
assert_eq!(
decoded.verified_payment_address(&P256Verifier),
Ok(PAYEE),
"a record verified second-hand must verify from its bytes alone"
);
}