From 15f970093b6c9768bb71544429d6c9ec6f783412 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Mon, 24 Aug 2026 12:47:59 -0700 Subject: [PATCH 1/3] HIP-150: value multiplier tickets and the multiplier on burned sessions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit HIP-150 Decision 1 grants a hotspot a multiplier on the data credits derived from its rewardable bytes, raising both what a payer burns for its data and what its deployer earns, in the same proportion. Adds to poc_mobile: value_multiplier_ticket_req_v1 the grant, signed by an authorized issuing key value_multiplier_ticket_resp_v1 value_multiplier_ticket_ingest_report_v1 what ingest writes to s3 verified_value_multiplier_ticket_report_v1 the verifier's outcome, recording rejections as well as grants so both are auditable verified_value_multiplier_ticket_status rpc submit_value_multiplier_ticket The multiplier is a helium.Decimal rather than a scaled integer. A message field is nullable, so an absent multiplier is absent rather than a reserved value of a scalar — there is no sentinel to misread. The accepted range is deliberately not stated here. It is policy the oracles enforce and can change without a schema change, so the wire format does not encode it; the verifier emits a verified report for a rejected multiplier, keeping the rejection publicly auditable rather than dropping it at ingest. valid_data_transfer_session gains the same type. num_dcs stays the post-multiplier figure — what was actually burned, and what the reward path already reads — so this field is what makes the pre-multiplier value recoverable. Absent means no multiplier was in force or the record predates HIP-150; readers resolve absent to 1, which needs no sentinel and keeps old records decoding correctly. network_key_role gains value_multiplier. Kept separate from mobile_carrier so that holding a carrier key does not confer the ability to grant a hotspot a reward multiplier. --- src/service/mobile_config.proto | 4 ++ src/service/packet_verifier.proto | 9 ++++ src/service/poc_mobile.proto | 68 +++++++++++++++++++++++++++++++ 3 files changed, 81 insertions(+) diff --git a/src/service/mobile_config.proto b/src/service/mobile_config.proto index 5aa34b0f..150599ec 100644 --- a/src/service/mobile_config.proto +++ b/src/service/mobile_config.proto @@ -369,6 +369,10 @@ enum network_key_role { mobile_pcs = 2; // Key for signing ban requests network_key_role_banning = 3; + // Key for signing HIP-150 value multiplier tickets. Separate from + // mobile_carrier so that holding a carrier key does not confer the ability to + // grant a hotspot a reward multiplier. + network_key_role_value_multiplier = 4; } enum device_type { diff --git a/src/service/packet_verifier.proto b/src/service/packet_verifier.proto index 04adec5f..73e30154 100644 --- a/src/service/packet_verifier.proto +++ b/src/service/packet_verifier.proto @@ -2,6 +2,8 @@ syntax = "proto3"; package helium.packet_verifier; +import "decimal.proto"; + message valid_packet { uint32 payload_size = 1; bytes gateway = 2; @@ -36,4 +38,11 @@ message valid_data_transfer_session { uint64 rewardable_bytes = 8; // Timestamp in millis dated when burn transaction is confirmed uint64 burn_timestamp = 9; + // HIP-150: the value multiplier applied to derive num_dcs. num_dcs is the + // post-multiplier figure — what was actually burned — and this field is what + // makes the pre-multiplier value recoverable. + // + // Absent means no multiplier was in force, or the record predates HIP-150. + // Readers resolve absent to 1. + Decimal multiplier = 10; } diff --git a/src/service/poc_mobile.proto b/src/service/poc_mobile.proto index 5947784d..cb573562 100644 --- a/src/service/poc_mobile.proto +++ b/src/service/poc_mobile.proto @@ -536,6 +536,8 @@ service poc_mobile { rpc submit_ban(ban_req_v1) returns (ban_resp_v1); rpc submit_enabled_carriers_info(enabled_carriers_info_req_v1) returns (enabled_carriers_info_resp_v1); + rpc submit_value_multiplier_ticket(value_multiplier_ticket_req_v1) + returns (value_multiplier_ticket_resp_v1); } message ban_resp_v1 { uint64 timestamp_ms = 1; } @@ -610,6 +612,72 @@ message verified_ban_ingest_report_v1 { verified_ban_ingest_report_status status = 3; } +// HIP-150: a grant of a data credit value multiplier to one on-chain hotspot. +// +// The multiplier applies to the data credits derived from that hotspot's +// rewardable bytes, not to the bytes themselves. It raises both what a payer +// burns for the hotspot's data and what its deployer earns, in the same +// proportion. +// +// Absence of a ticket means the hotspot is at 1x. There is no ticket that means +// "no multiplier" — to return a hotspot to 1x, issue a ticket granting exactly +// 1. +message value_multiplier_ticket_req_v1 { + // Address of the hotspot the multiplier attaches to + bytes hotspot_pubkey = 1; + // The multiplier granted. The accepted range is policy enforced by the + // oracles, not fixed by this schema. + // + // Required: a ticket that grants nothing is meaningless, and an absent + // multiplier here is rejected. Absence means "no multiplier in force" only + // where a multiplier is looked up, never on the grant itself. + // + // Applied to a data credit count the value is rounded down, so a payer never + // burns more than the multiplier earns. + Decimal multiplier = 2; + // Timestamp in milliseconds the ticket was issued + uint64 timestamp_ms = 3; + // Free-form provenance for the public record: agreement id, venue, reason for + // the grant. HIP-150 requires every multiplier in force to be externally + // auditable. + string message = 4; + // Signer of the message. Must be a key authorized to issue tickets. + bytes signer_pubkey = 5; + bytes signature = 6; +} + +message value_multiplier_ticket_resp_v1 { uint64 timestamp_ms = 1; } + +// All value multiplier tickets received by the Ingester. +message value_multiplier_ticket_ingest_report_v1 { + // Timestamp in milliseconds since unix epoch + uint64 received_timestamp_ms = 1; + value_multiplier_ticket_req_v1 report = 2; +} + +enum verified_value_multiplier_ticket_status { + verified_value_multiplier_ticket_status_valid = 0; + // The signer is not authorized to issue tickets + verified_value_multiplier_ticket_status_invalid_signer = 1; + // The multiplier is absent, unparseable, or outside the range the oracles + // accept + verified_value_multiplier_ticket_status_invalid_multiplier = 2; + // The hotspot is not known on chain + verified_value_multiplier_ticket_status_invalid_hotspot_key = 3; +} + +// Value multiplier tickets after verification, recording the outcome of every +// ticket including the rejected ones, so a rejection is as auditable as a +// grant. +message verified_value_multiplier_ticket_report_v1 { + // Timestamp in milliseconds the report was verified by oracles + uint64 verified_timestamp_ms = 1; + // The verified report + value_multiplier_ticket_ingest_report_v1 report = 2; + + verified_value_multiplier_ticket_status status = 3; +} + message unique_connections_req_v1 { // Wifi Radio pubkey bytes pubkey = 1; From 79c477100e1571649c50a6f6a36db7423277fec6 Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Mon, 24 Aug 2026 17:49:55 -0700 Subject: [PATCH 2/3] Rename to data_transfer_multiplier, and harden the ticket status enum Rename: value_multiplier -> data_transfer_multiplier throughout. "Value multiplier" does not say what is multiplied; the multiplier applies to the data credits derived from a hotspot's rewardable bytes, and the new name says so. Covers the ticket messages, the verified report and its status enum, the rpc, and the network_key_role variant. Reserve zero in the status enum as `unspecified` rather than `valid`. Proto3 cannot distinguish an absent field from one explicitly set to zero, so with `valid = 0` a producer that forgets to set the status, a bug that clears it, or a partially-constructed message all decode as a grant. The sibling verified-report enums in this file put `valid` at zero and fail open the same way; that is not followed here, and the comment says why so it is not "fixed" back later. Matches the newer enums in this file (carrier_id_v2_unspecified, ban_reason_unknown). Add `invalid_timestamp` for a ticket dated in the future, or old enough that honouring it would mean acting on a replayed or long-delayed grant. A signature stays valid forever, so without a freshness window a captured ticket is replayable indefinitely. --- src/service/mobile_config.proto | 4 --- src/service/packet_verifier.proto | 6 ++-- src/service/poc_mobile.proto | 52 ++++++++++++++++++++----------- 3 files changed, 37 insertions(+), 25 deletions(-) diff --git a/src/service/mobile_config.proto b/src/service/mobile_config.proto index 150599ec..5aa34b0f 100644 --- a/src/service/mobile_config.proto +++ b/src/service/mobile_config.proto @@ -369,10 +369,6 @@ enum network_key_role { mobile_pcs = 2; // Key for signing ban requests network_key_role_banning = 3; - // Key for signing HIP-150 value multiplier tickets. Separate from - // mobile_carrier so that holding a carrier key does not confer the ability to - // grant a hotspot a reward multiplier. - network_key_role_value_multiplier = 4; } enum device_type { diff --git a/src/service/packet_verifier.proto b/src/service/packet_verifier.proto index 73e30154..a79570c7 100644 --- a/src/service/packet_verifier.proto +++ b/src/service/packet_verifier.proto @@ -38,9 +38,9 @@ message valid_data_transfer_session { uint64 rewardable_bytes = 8; // Timestamp in millis dated when burn transaction is confirmed uint64 burn_timestamp = 9; - // HIP-150: the value multiplier applied to derive num_dcs. num_dcs is the - // post-multiplier figure — what was actually burned — and this field is what - // makes the pre-multiplier value recoverable. + // HIP-150: the data transfer multiplier applied to derive num_dcs. num_dcs + // is the post-multiplier figure — what was actually burned — and this field + // is what makes the pre-multiplier value recoverable. // // Absent means no multiplier was in force, or the record predates HIP-150. // Readers resolve absent to 1. diff --git a/src/service/poc_mobile.proto b/src/service/poc_mobile.proto index cb573562..3e432b4c 100644 --- a/src/service/poc_mobile.proto +++ b/src/service/poc_mobile.proto @@ -536,8 +536,9 @@ service poc_mobile { rpc submit_ban(ban_req_v1) returns (ban_resp_v1); rpc submit_enabled_carriers_info(enabled_carriers_info_req_v1) returns (enabled_carriers_info_resp_v1); - rpc submit_value_multiplier_ticket(value_multiplier_ticket_req_v1) - returns (value_multiplier_ticket_resp_v1); + rpc submit_data_transfer_multiplier_ticket( + data_transfer_multiplier_ticket_req_v1) + returns (data_transfer_multiplier_ticket_resp_v1); } message ban_resp_v1 { uint64 timestamp_ms = 1; } @@ -612,7 +613,7 @@ message verified_ban_ingest_report_v1 { verified_ban_ingest_report_status status = 3; } -// HIP-150: a grant of a data credit value multiplier to one on-chain hotspot. +// HIP-150: a grant of a data transfer multiplier to one on-chain hotspot. // // The multiplier applies to the data credits derived from that hotspot's // rewardable bytes, not to the bytes themselves. It raises both what a payer @@ -620,9 +621,9 @@ message verified_ban_ingest_report_v1 { // proportion. // // Absence of a ticket means the hotspot is at 1x. There is no ticket that means -// "no multiplier" — to return a hotspot to 1x, issue a ticket granting exactly -// 1. -message value_multiplier_ticket_req_v1 { +// "no multiplier" — to return a hotspot to 1x, issue a ticket granting +// exactly 1. +message data_transfer_multiplier_ticket_req_v1 { // Address of the hotspot the multiplier attaches to bytes hotspot_pubkey = 1; // The multiplier granted. The accepted range is policy enforced by the @@ -646,36 +647,51 @@ message value_multiplier_ticket_req_v1 { bytes signature = 6; } -message value_multiplier_ticket_resp_v1 { uint64 timestamp_ms = 1; } +message data_transfer_multiplier_ticket_resp_v1 { uint64 timestamp_ms = 1; } -// All value multiplier tickets received by the Ingester. -message value_multiplier_ticket_ingest_report_v1 { +// All data transfer multiplier tickets received by the Ingester. +message data_transfer_multiplier_ticket_ingest_report_v1 { // Timestamp in milliseconds since unix epoch uint64 received_timestamp_ms = 1; - value_multiplier_ticket_req_v1 report = 2; + data_transfer_multiplier_ticket_req_v1 report = 2; } -enum verified_value_multiplier_ticket_status { - verified_value_multiplier_ticket_status_valid = 0; +enum verified_data_transfer_multiplier_ticket_status { + // Never written by a producer. Zero is reserved so that an unset, cleared, + // or dropped status cannot decode as a grant: proto3 cannot distinguish an + // absent field from one explicitly set to zero, and this field decides + // whether a hotspot's rewards are multiplied. Consumers must treat it as + // invalid. + // + // The sibling verified-report enums in this file put `valid` at zero. That + // is deliberately not followed here — those fail open on a malformed + // message, and this is the one that gates a reward multiplier. + verified_data_transfer_multiplier_ticket_status_unspecified = 0; + verified_data_transfer_multiplier_ticket_status_valid = 1; // The signer is not authorized to issue tickets - verified_value_multiplier_ticket_status_invalid_signer = 1; + verified_data_transfer_multiplier_ticket_status_invalid_signer = 2; // The multiplier is absent, unparseable, or outside the range the oracles // accept - verified_value_multiplier_ticket_status_invalid_multiplier = 2; + verified_data_transfer_multiplier_ticket_status_invalid_multiplier = 3; // The hotspot is not known on chain - verified_value_multiplier_ticket_status_invalid_hotspot_key = 3; + verified_data_transfer_multiplier_ticket_status_invalid_hotspot_key = 4; + // The signed timestamp is outside the window the oracles accept: dated in the + // future, or old enough that honouring it would mean acting on a replayed or + // long-delayed grant. A signature stays valid forever; this is what stops a + // captured ticket from being useful later. + verified_data_transfer_multiplier_ticket_status_invalid_timestamp = 5; } // Value multiplier tickets after verification, recording the outcome of every // ticket including the rejected ones, so a rejection is as auditable as a // grant. -message verified_value_multiplier_ticket_report_v1 { +message verified_data_transfer_multiplier_ticket_report_v1 { // Timestamp in milliseconds the report was verified by oracles uint64 verified_timestamp_ms = 1; // The verified report - value_multiplier_ticket_ingest_report_v1 report = 2; + data_transfer_multiplier_ticket_ingest_report_v1 report = 2; - verified_value_multiplier_ticket_status status = 3; + verified_data_transfer_multiplier_ticket_status status = 3; } message unique_connections_req_v1 { From 0e1d14433cc7350cc4a13938fe23b6f12e4b3d7d Mon Sep 17 00:00:00 2001 From: Michael Jeffrey Date: Tue, 25 Aug 2026 11:51:37 -0700 Subject: [PATCH 3/3] Drop the unspecified variant from the ticket status enum MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The verified report is written only by our own packet verifier and read as an audit artifact; it never arrives from an untrusted party. Reserving zero was defending against a bug in a single producer we control, at the cost of an impossible state every consumer has to match on and a divergence from the twelve sibling verified-report enums in this file. The attacker-facing message is data_transfer_multiplier_ticket_req_v1, and it has no status field — what protects it is the signature, the allow-list and the freshness window, none of which this variant contributed to. `valid` returns to zero, matching every other verified-report enum here. invalid_timestamp is unchanged and renumbers with the rest; nothing consumes these values yet, so the renumber is free. --- src/service/poc_mobile.proto | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/src/service/poc_mobile.proto b/src/service/poc_mobile.proto index 3e432b4c..43152ac9 100644 --- a/src/service/poc_mobile.proto +++ b/src/service/poc_mobile.proto @@ -657,29 +657,19 @@ message data_transfer_multiplier_ticket_ingest_report_v1 { } enum verified_data_transfer_multiplier_ticket_status { - // Never written by a producer. Zero is reserved so that an unset, cleared, - // or dropped status cannot decode as a grant: proto3 cannot distinguish an - // absent field from one explicitly set to zero, and this field decides - // whether a hotspot's rewards are multiplied. Consumers must treat it as - // invalid. - // - // The sibling verified-report enums in this file put `valid` at zero. That - // is deliberately not followed here — those fail open on a malformed - // message, and this is the one that gates a reward multiplier. - verified_data_transfer_multiplier_ticket_status_unspecified = 0; - verified_data_transfer_multiplier_ticket_status_valid = 1; + verified_data_transfer_multiplier_ticket_status_valid = 0; // The signer is not authorized to issue tickets - verified_data_transfer_multiplier_ticket_status_invalid_signer = 2; + verified_data_transfer_multiplier_ticket_status_invalid_signer = 1; // The multiplier is absent, unparseable, or outside the range the oracles // accept - verified_data_transfer_multiplier_ticket_status_invalid_multiplier = 3; + verified_data_transfer_multiplier_ticket_status_invalid_multiplier = 2; // The hotspot is not known on chain - verified_data_transfer_multiplier_ticket_status_invalid_hotspot_key = 4; + verified_data_transfer_multiplier_ticket_status_invalid_hotspot_key = 3; // The signed timestamp is outside the window the oracles accept: dated in the // future, or old enough that honouring it would mean acting on a replayed or // long-delayed grant. A signature stays valid forever; this is what stops a // captured ticket from being useful later. - verified_data_transfer_multiplier_ticket_status_invalid_timestamp = 5; + verified_data_transfer_multiplier_ticket_status_invalid_timestamp = 4; } // Value multiplier tickets after verification, recording the outcome of every