Skip to content
Merged
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
38 changes: 19 additions & 19 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 7 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,10 @@ anchor-lang = { git = "https://github.com/madninja/anchor.git", branch = "madnin
# beacon = { path = "../proto/beacon" }

[patch.'https://github.com/helium/proto']
# helium-proto = { git = "https://www.github.com/helium/proto.git", branch = "gateway-info-v4" }
# beacon = { git = "https://www.github.com/helium/proto.git", branch = "gateway-info-v4" }
# msg-signature = { git = "https://www.github.com/helium/proto.git", branch = "gateway-info-v4" }
# HIP-150: the data transfer multiplier ticket messages live on proto's
# mj/hip-150 (helium/proto#483) and are not on master yet.
# REVERT BEFORE MERGING mj/hip-150 TO main — a feature-branch patch must not
# reach main. Tracked on the pre-deploy checklist.
helium-proto = { git = "https://www.github.com/helium/proto.git", branch = "mj/hip-150" }
beacon = { git = "https://www.github.com/helium/proto.git", branch = "mj/hip-150" }
msg-signature = { git = "https://www.github.com/helium/proto.git", branch = "mj/hip-150" }
1 change: 1 addition & 0 deletions file_store_oracles/src/file_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,5 +153,6 @@ make_string_mapped_enum! {
EntityOwnershipChangeReport => "entity_ownership_change_report",
EntityRewardDestinationChangeReport => "entity_reward_destination_change_report",
EnabledCarriersInfoReport => "enabled_carriers_report",
DataTransferMultiplierTicketIngestReport => "data_transfer_multiplier_ticket_ingest_report",
}
}
21 changes: 21 additions & 0 deletions file_store_oracles/src/mobile/data_transfer_multiplier.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//! HIP-150 data transfer multipliers, and the tickets that grant them.

use std::time::Duration;

/// How far ahead of the receiving oracle's clock a ticket may be stamped.
///
/// Clients do not share a clock with ingest, so a ticket signed at what the
/// client believes is "now" can arrive stamped slightly in the future. Without
/// some tolerance those are rejected as post-dated, which is a confusing failure
/// for an honest client with a drifting clock.
///
/// One minute is enough for ordinary NTP-less drift and short enough that it
/// buys an attacker nothing: a post-dated ticket still ages out of the freshness
/// window at the same rate, it just starts a minute earlier.
///
/// **Shared deliberately.** Ingest and the packet verifier both check freshness,
/// and the verifier measures a ticket's age against the timestamp *ingest*
/// stamped on it. If ingest tolerated drift the verifier did not, every ticket
/// ingest accepted from a fast client would then be refused downstream — so the
/// two must use one value, not two settings that can be configured apart.
pub const MAX_CLOCK_DRIFT: Duration = Duration::from_secs(60);
5 changes: 5 additions & 0 deletions file_store_oracles/src/mobile/mobile_transfer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ impl From<ValidDataTransferSession> for proto::ValidDataTransferSession {
last_timestamp: v.last_timestamp.encode_timestamp_millis(),
rewardable_bytes: v.rewardable_bytes,
burn_timestamp: v.burn_timestamp.encode_timestamp_millis(),
// HIP-150: populated once mobile-packet-verifier applies multipliers.
// Absent means no multiplier was in force, which is what every
// session is until then — so this preserves current behaviour
// exactly rather than asserting a 1x that was never looked up.
multiplier: None,
}
}
}
1 change: 1 addition & 0 deletions file_store_oracles/src/mobile/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub mod coverage;
pub mod data_transfer_multiplier;
pub mod hex_boost;
pub mod mobile_ban;
pub mod mobile_radio_invalidated_threshold;
Expand Down
5 changes: 5 additions & 0 deletions file_store_oracles/src/traits/file_sink_write.rs
Original file line number Diff line number Diff line change
Expand Up @@ -337,3 +337,8 @@ impl_file_sink!(
FileType::EnabledCarriersInfoReport.to_str(),
"enabled_carriers_report"
);
impl_file_sink!(
poc_mobile::DataTransferMultiplierTicketIngestReportV1,
FileType::DataTransferMultiplierTicketIngestReport.to_str(),
"data_transfer_multiplier_ticket_ingest_report"
);
21 changes: 21 additions & 0 deletions ingest/pkg/settings-template.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,27 @@ network = "mainnet"
# Ignored in "chain" mode.
carrier_authorized_keys = "key1,key2"

# HIP-150. Comma-separated b58 public keys authorized to issue data transfer
# multiplier tickets. Deliberately separate from carrier_authorized_keys, so a
# carrier key cannot grant a hotspot a reward multiplier.
#
# Unlike carrier_authorized_keys this may be empty, and defaults to empty: the
# oracle release ships before any ticket can be issued. Empty fails closed —
# every ticket is rejected — and warns at startup.
#
# data_transfer_multiplier_authorized_keys = "key1,key2"

# HIP-150. How old a ticket's signed timestamp may be before ingest refuses it.
# A signature never expires, so this is what stops a ticket captured off the
# wire from being replayed later. Defaults to 10 minutes.
#
# A ticket stamped slightly in the future is treated as current rather than
# refused, since clients do not share a clock with ingest. That allowance is a
# constant shared with the packet verifier, not a setting: the two check
# freshness against the same timestamp and must not be configured apart.
#
# data_transfer_multiplier_ticket_max_age = "10 minutes"

[output]
# Output bucket for ingested data

Expand Down
50 changes: 47 additions & 3 deletions ingest/src/authorization.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,37 @@ use helium_proto::services::mobile_config::NetworkKeyRole;
#[derive(Debug, Clone, Default)]
pub struct AuthorizedKeys {
carrier: HashSet<PublicKeyBinary>,
/// HIP-150 ticket issuers. Kept separate from `carrier` so that holding a
/// carrier key does not confer the ability to grant a hotspot a reward
/// multiplier. May be empty, in which case no ticket is accepted.
data_transfer_multiplier: HashSet<PublicKeyBinary>,
}

impl AuthorizedKeys {
pub fn new(carrier: HashSet<PublicKeyBinary>) -> Self {
Self { carrier }
pub fn new(
carrier: HashSet<PublicKeyBinary>,
data_transfer_multiplier: HashSet<PublicKeyBinary>,
) -> Self {
Self {
carrier,
data_transfer_multiplier,
}
}
}

/// A role-scoped authorization check. Kept as a trait (rather than using
/// [`AuthorizedKeys`] directly at call sites) so tests can substitute a mock.
pub trait AuthorizationVerifier: Send + Sync + 'static {
fn is_authorized(&self, address: &PublicKeyBinary, role: NetworkKeyRole) -> bool;

/// HIP-150: may this key issue data transfer multiplier tickets?
///
/// Deliberately not a `NetworkKeyRole` arm. That enum comes from
/// `mobile_config.proto`, and mobile-config is decommissioned in this
/// stack — it survives only as this trait's role parameter. Adding a
/// variant would grow a dead service's enum, so ticket authorization gets
/// its own check instead.
fn is_ticket_signer(&self, address: &PublicKeyBinary) -> bool;
}

impl AuthorizationVerifier for AuthorizedKeys {
Expand All @@ -39,6 +58,10 @@ impl AuthorizationVerifier for AuthorizedKeys {
_ => false,
}
}

fn is_ticket_signer(&self, address: &PublicKeyBinary) -> bool {
self.data_transfer_multiplier.contains(address)
}
}

#[cfg(test)]
Expand All @@ -51,7 +74,7 @@ mod tests {

#[test]
fn authorizes_only_configured_keys_per_role() {
let keys = AuthorizedKeys::new(HashSet::from([key(1)]));
let keys = AuthorizedKeys::new(HashSet::from([key(1)]), HashSet::new());

assert!(keys.is_authorized(&key(1), NetworkKeyRole::MobileCarrier));
assert!(!keys.is_authorized(&key(2), NetworkKeyRole::MobileCarrier));
Expand All @@ -60,4 +83,25 @@ mod tests {
assert!(!keys.is_authorized(&key(1), NetworkKeyRole::Banning));
assert!(!keys.is_authorized(&key(1), NetworkKeyRole::MobileRouter));
}

#[test]
fn ticket_signing_is_separate_from_the_carrier_allow_list() {
let keys = AuthorizedKeys::new(HashSet::from([key(1)]), HashSet::from([key(2)]));

// A carrier key cannot grant a multiplier...
assert!(keys.is_authorized(&key(1), NetworkKeyRole::MobileCarrier));
assert!(!keys.is_ticket_signer(&key(1)));

// ...and a ticket signer is not thereby a carrier.
assert!(keys.is_ticket_signer(&key(2)));
assert!(!keys.is_authorized(&key(2), NetworkKeyRole::MobileCarrier));
}

#[test]
fn empty_ticket_allow_list_rejects_every_signer() {
let keys = AuthorizedKeys::new(HashSet::from([key(1)]), HashSet::new());

assert!(!keys.is_ticket_signer(&key(1)));
assert!(!keys.is_ticket_signer(&key(2)));
}
}
Loading