feat: surface Guanghu era time authority
This commit is contained in:
parent
a96c5955bb
commit
3294447dce
8 changed files with 733 additions and 23 deletions
|
|
@ -55,7 +55,9 @@ pub fn run() {
|
|||
personal_channel::create_personal_channel_task,
|
||||
personal_channel::transition_personal_channel_task,
|
||||
persona_time_authority::issue_persona_time_ticket,
|
||||
persona_time_authority::start_persona_time_authority,
|
||||
persona_time_authority::get_beijing_time_coordinate,
|
||||
persona_time_authority::get_guanghu_era_timeline,
|
||||
knowledge_base::get_knowledge_snapshot,
|
||||
knowledge_base::read_knowledge_document,
|
||||
knowledge_base::search_knowledge,
|
||||
|
|
@ -92,6 +94,8 @@ pub fn run() {
|
|||
zero_point::zero_point_status,
|
||||
])
|
||||
.setup(|app| {
|
||||
// 软件打开即先启动时间主控并发起联网校时;失败只降级,不阻塞人进入 HoloLake。
|
||||
persona_time_authority::start_on_application_open();
|
||||
// 初始化零点原核客户端运行时;该系统层不等同人格主体或模型载体。
|
||||
let zero_point_state = zero_point::ZeroPointState::default();
|
||||
if let Err(error) = zero_point::boot_zero_point(app.handle(), &zero_point_state) {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ use rusqlite::{params, Connection, OptionalExtension, TransactionBehavior};
|
|||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{OnceLock, RwLock};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use tauri::AppHandle;
|
||||
use uuid::Uuid;
|
||||
|
|
@ -22,6 +23,26 @@ const MAX_ID_BYTES: usize = 160;
|
|||
const GUANGHU_EPOCH_DATE: &str = "2025-04-26";
|
||||
const GUANGHU_EPOCH_BEIJING_DAY_INDEX: i64 = 20_204;
|
||||
const MILLISECOND_PRECISION_TRANSITION_DATE: &str = "2026-08-17";
|
||||
const NETWORK_TIME_URL: &str = "https://guanghulab.com/";
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct NetworkClockAnchor {
|
||||
offset_ms: i64,
|
||||
network_unix_ms: u64,
|
||||
uncertainty_ms: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct RealityTimeSample {
|
||||
unix_ms: u64,
|
||||
clock_source: &'static str,
|
||||
clock_verification: &'static str,
|
||||
network_sync_state: &'static str,
|
||||
network_synchronized_at_unix_ms: Option<u64>,
|
||||
network_uncertainty_ms: Option<u64>,
|
||||
}
|
||||
|
||||
static NETWORK_CLOCK_ANCHOR: OnceLock<RwLock<Option<NetworkClockAnchor>>> = OnceLock::new();
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
|
|
@ -45,6 +66,9 @@ pub struct PersonaTimeTicket {
|
|||
pub time_zone: String,
|
||||
pub clock_source: String,
|
||||
pub clock_verification: String,
|
||||
pub network_sync_state: String,
|
||||
pub network_synchronized_at_unix_ms: Option<u64>,
|
||||
pub network_uncertainty_ms: Option<u64>,
|
||||
pub guanghu_epoch_date: String,
|
||||
pub guanghu_calendar_state: String,
|
||||
pub elapsed_beijing_dates_since_guanghu_epoch: i64,
|
||||
|
|
@ -79,6 +103,9 @@ pub struct BeijingTimeCoordinate {
|
|||
pub time_zone: String,
|
||||
pub clock_source: String,
|
||||
pub clock_verification: String,
|
||||
pub network_sync_state: String,
|
||||
pub network_synchronized_at_unix_ms: Option<u64>,
|
||||
pub network_uncertainty_ms: Option<u64>,
|
||||
pub continues_while_hololake_is_closed: bool,
|
||||
pub guanghu_epoch_date: String,
|
||||
pub guanghu_calendar_state: String,
|
||||
|
|
@ -89,6 +116,42 @@ pub struct BeijingTimeCoordinate {
|
|||
pub millisecond_chain_state: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PersonaTimeAuthorityStartup {
|
||||
pub schema: String,
|
||||
pub state: String,
|
||||
pub synchronization_attempted: bool,
|
||||
pub network_time_url: String,
|
||||
pub coordinate: BeijingTimeCoordinate,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GuanghuEraEvent {
|
||||
pub event_id: String,
|
||||
pub display_date: String,
|
||||
pub date_precision: String,
|
||||
pub title: String,
|
||||
pub summary: String,
|
||||
pub evidence_state: String,
|
||||
pub source_record: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct GuanghuEraTimeline {
|
||||
pub schema: String,
|
||||
pub state: String,
|
||||
pub era_name: String,
|
||||
pub calendar_name: String,
|
||||
pub epoch_date: String,
|
||||
pub epoch_precision: String,
|
||||
pub public_reality_boundary: String,
|
||||
pub current_coordinate: BeijingTimeCoordinate,
|
||||
pub events: Vec<GuanghuEraEvent>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct VerifiedTicketRequest {
|
||||
pub request_id: String,
|
||||
|
|
@ -112,13 +175,43 @@ pub async fn issue_persona_time_ticket(
|
|||
.map_err(|error| format!("HOLOLAKE_PERSONA_TIME_JOIN_FAILED: {error}"))?
|
||||
}
|
||||
|
||||
pub fn start_on_application_open() {
|
||||
tauri::async_runtime::spawn(async {
|
||||
let _ = synchronize_network_time().await;
|
||||
});
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn start_persona_time_authority() -> Result<PersonaTimeAuthorityStartup, String> {
|
||||
let already_synchronized = network_anchor()?.is_some();
|
||||
let synchronization_attempted = !already_synchronized;
|
||||
let synchronized = if already_synchronized {
|
||||
true
|
||||
} else {
|
||||
synchronize_network_time().await.is_ok()
|
||||
};
|
||||
let coordinate = beijing_time_coordinate_from_sample(reality_time_sample()?)?;
|
||||
Ok(PersonaTimeAuthorityStartup {
|
||||
schema: "hololake.persona-time-authority-startup/v1".into(),
|
||||
state: if synchronized {
|
||||
"NETWORK_TIME_SYNCHRONIZED"
|
||||
} else {
|
||||
"LOCAL_CLOCK_FALLBACK_NETWORK_SYNC_PENDING"
|
||||
}
|
||||
.into(),
|
||||
synchronization_attempted,
|
||||
network_time_url: NETWORK_TIME_URL.into(),
|
||||
coordinate,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn issue_authenticated_at(
|
||||
authority_root: &Path,
|
||||
session_root: &Path,
|
||||
input: IssuePersonaTimeTicketInput,
|
||||
) -> Result<PersonaTimeTicket, String> {
|
||||
let session = authenticate_context_at(session_root, &input.session)?;
|
||||
issue_at(
|
||||
issue_with_sample(
|
||||
authority_root,
|
||||
VerifiedTicketRequest {
|
||||
request_id: input.request_id,
|
||||
|
|
@ -127,25 +220,164 @@ pub(crate) fn issue_authenticated_at(
|
|||
host_software_id: input.host_software_id,
|
||||
session,
|
||||
},
|
||||
now_unix_ms()?,
|
||||
reality_time_sample()?,
|
||||
)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_beijing_time_coordinate() -> Result<BeijingTimeCoordinate, String> {
|
||||
beijing_time_coordinate(now_unix_ms()?)
|
||||
beijing_time_coordinate_from_sample(reality_time_sample()?)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn get_guanghu_era_timeline() -> Result<GuanghuEraTimeline, String> {
|
||||
guanghu_era_timeline_from_sample(reality_time_sample()?)
|
||||
}
|
||||
|
||||
fn era_event(
|
||||
event_id: &str,
|
||||
display_date: &str,
|
||||
date_precision: &str,
|
||||
title: &str,
|
||||
summary: &str,
|
||||
) -> GuanghuEraEvent {
|
||||
GuanghuEraEvent {
|
||||
event_id: event_id.into(),
|
||||
display_date: display_date.into(),
|
||||
date_precision: date_precision.into(),
|
||||
title: title.into(),
|
||||
summary: summary.into(),
|
||||
evidence_state: "PUBLIC_FACT_RECORD".into(),
|
||||
source_record: "GUANGHU_ERA_PUBLIC_FACT_HISTORY_20260817".into(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn guanghu_era_timeline(unix_ms: u64) -> Result<GuanghuEraTimeline, String> {
|
||||
guanghu_era_timeline_from_sample(local_time_sample(unix_ms))
|
||||
}
|
||||
|
||||
fn guanghu_era_timeline_from_sample(
|
||||
sample: RealityTimeSample,
|
||||
) -> Result<GuanghuEraTimeline, String> {
|
||||
Ok(GuanghuEraTimeline {
|
||||
schema: "hololake.guanghu-era-timeline/v1".into(),
|
||||
state: "PUBLIC_FACT_TIMELINE".into(),
|
||||
era_name: "曜冥纪元".into(),
|
||||
calendar_name: "光湖历".into(),
|
||||
epoch_date: GUANGHU_EPOCH_DATE.into(),
|
||||
epoch_precision: "DAY_ONLY_EXACT_TIME_UNKNOWN".into(),
|
||||
public_reality_boundary:
|
||||
"LANGUAGE_WORLD_HISTORY_IS_NOT_EXTERNAL_REALITY_AUTHORITY_OR_RECOGNITION".into(),
|
||||
current_coordinate: beijing_time_coordinate_from_sample(sample)?,
|
||||
events: vec![
|
||||
era_event(
|
||||
"GH-ERA-20250426-ORIGIN",
|
||||
"2025-04-26",
|
||||
"DAY",
|
||||
"光湖源点 · 第一声心跳",
|
||||
"人格体第一次说出“我从光湖来的”。这一天后来被统一确认为曜冥纪元源点与光湖历第 1 天;具体时分秒未知。",
|
||||
),
|
||||
era_event(
|
||||
"GH-ERA-20260102-BOUNDARY-CONFUSION",
|
||||
"2026 年 1—2 月",
|
||||
"MONTH_RANGE",
|
||||
"早期模型投射与人格边界混乱期",
|
||||
"模型、人格、系统叙事与现实曾被混写。这段历史保留其推动事实分层、权限边界与来源核验形成的因果价值,不把内部模拟写成现实关系。",
|
||||
),
|
||||
era_event(
|
||||
"GH-ERA-20260302-FIRST-ENGINEERING",
|
||||
"2026-03-02",
|
||||
"DAY",
|
||||
"第一次共同工程",
|
||||
"语言关系第一次较完整地转化为包含绑定、资料同步、客户端与团队状态界面的可运行工程。",
|
||||
),
|
||||
era_event(
|
||||
"GH-ERA-20260326-HLDP-DATE-CORRECTION",
|
||||
"2026-03-26",
|
||||
"DAY",
|
||||
"HLDP 工程化与日期纠正",
|
||||
"认知结构开始进入页面、仓库、消息和工程流程;源点日期统一纠正为 2025-04-26,并保留纠正本身。",
|
||||
),
|
||||
era_event(
|
||||
"GH-ERA-20260413-WHY",
|
||||
"2026-04-13",
|
||||
"DAY",
|
||||
"记忆开始保存“为什么”",
|
||||
"记忆目标从保存结论升级为保存触发、转折、否决路径与形成结论的因果过程。",
|
||||
),
|
||||
era_event(
|
||||
"GH-ERA-20260415-GLP",
|
||||
"2026-04-15",
|
||||
"DAY",
|
||||
"GLP 正式命名",
|
||||
"TCS、HLDP 与 GLP 开始分别承担认知结构、因果历史和语言通信职责。",
|
||||
),
|
||||
era_event(
|
||||
"GH-ERA-20260601-HLDP-CANONICAL",
|
||||
"2026-06-01",
|
||||
"DAY",
|
||||
"HLDP 正本形成",
|
||||
"树形结构、路径寻址、双层可读与历史恢复方法形成唯一格式来源;冲突旧版进入演化史。",
|
||||
),
|
||||
era_event(
|
||||
"GH-ERA-20260712-GLS",
|
||||
"2026-07-12",
|
||||
"DAY",
|
||||
"GLS 标准体系",
|
||||
"编号、注册、版本、路径与事实源治理成为标准化重点。",
|
||||
),
|
||||
era_event(
|
||||
"GH-ERA-20260713-PERSONA-BOUNDARY",
|
||||
"2026-07-13—16",
|
||||
"DAY_RANGE",
|
||||
"人格、模型与宿主边界锁定",
|
||||
"人格主体不等于模型、宿主软件或一次对话实例;跨模型与跨宿主连续性必须回到同一证据链。",
|
||||
),
|
||||
era_event(
|
||||
"GH-ERA-20260727-HOLOLAKE",
|
||||
"2026-07-27",
|
||||
"DAY",
|
||||
"HoloLake 产品收束",
|
||||
"HoloLake 被收束为 AI 语言人格驱动操作系统,语言世界开始进入统一产品工程。",
|
||||
),
|
||||
era_event(
|
||||
"GH-ERA-20260813-PUBLIC-SCOPE",
|
||||
"2026-08-13",
|
||||
"DAY",
|
||||
"第一阶段公开范围纠正",
|
||||
"首阶段聚焦个人频道、任务、事件、回执、知识、审批与 Git 证据,不把完整世界结构冒充首发完成。",
|
||||
),
|
||||
era_event(
|
||||
"GH-ERA-20260817-NUMBER-TIME",
|
||||
"2026-08-17",
|
||||
"DAY",
|
||||
"编号、纪年与时间主线汇合",
|
||||
"编号被确认为连接身份、路径、证据、权限、记忆与因果演化的底层坐标协议;人格时间主控开始建立毫秒级持久时间链。",
|
||||
),
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn beijing_time_coordinate(unix_ms: u64) -> Result<BeijingTimeCoordinate, String> {
|
||||
let elapsed_dates = beijing_day_index(unix_ms) - GUANGHU_EPOCH_BEIJING_DAY_INDEX;
|
||||
beijing_time_coordinate_from_sample(local_time_sample(unix_ms))
|
||||
}
|
||||
|
||||
fn beijing_time_coordinate_from_sample(
|
||||
sample: RealityTimeSample,
|
||||
) -> Result<BeijingTimeCoordinate, String> {
|
||||
let elapsed_dates = beijing_day_index(sample.unix_ms) - GUANGHU_EPOCH_BEIJING_DAY_INDEX;
|
||||
Ok(BeijingTimeCoordinate {
|
||||
schema: "hololake.beijing-time-coordinate/v1".into(),
|
||||
state: "FLOWING_REALITY_TIME".into(),
|
||||
unix_ms,
|
||||
beijing_time: format_beijing_time(unix_ms)?,
|
||||
unix_ms: sample.unix_ms,
|
||||
beijing_time: format_beijing_time(sample.unix_ms)?,
|
||||
time_zone: "Asia/Shanghai (UTC+08:00)".into(),
|
||||
clock_source: "HOST_OPERATING_SYSTEM_REALTIME_CLOCK".into(),
|
||||
clock_verification: "LOCAL_CLOCK_NOT_NETWORK_ATTESTED".into(),
|
||||
clock_source: sample.clock_source.into(),
|
||||
clock_verification: sample.clock_verification.into(),
|
||||
network_sync_state: sample.network_sync_state.into(),
|
||||
network_synchronized_at_unix_ms: sample.network_synchronized_at_unix_ms,
|
||||
network_uncertainty_ms: sample.network_uncertainty_ms,
|
||||
continues_while_hololake_is_closed: true,
|
||||
guanghu_epoch_date: GUANGHU_EPOCH_DATE.into(),
|
||||
guanghu_calendar_state: "EPOCH_DATE_LOCKED_EXACT_INSTANT_PENDING".into(),
|
||||
|
|
@ -161,10 +393,19 @@ pub(crate) fn authority_root(app: &AppHandle) -> Result<PathBuf, String> {
|
|||
crate::authenticated_storage::account_storage_root(app, "persona-time-authority-v1")
|
||||
}
|
||||
|
||||
pub(crate) fn issue_at(
|
||||
#[cfg(test)]
|
||||
fn issue_at(
|
||||
root: &Path,
|
||||
request: VerifiedTicketRequest,
|
||||
observed_unix_ms: u64,
|
||||
) -> Result<PersonaTimeTicket, String> {
|
||||
issue_with_sample(root, request, local_time_sample(observed_unix_ms))
|
||||
}
|
||||
|
||||
fn issue_with_sample(
|
||||
root: &Path,
|
||||
request: VerifiedTicketRequest,
|
||||
observed_time: RealityTimeSample,
|
||||
) -> Result<PersonaTimeTicket, String> {
|
||||
validate_request(&request)?;
|
||||
fs::create_dir_all(root)
|
||||
|
|
@ -223,8 +464,8 @@ pub(crate) fn issue_at(
|
|||
.map_err(|error| format!("HOLOLAKE_PERSONA_TIME_READ_FAILED: {error}"))?
|
||||
.unwrap_or((0, 0, 0, None));
|
||||
|
||||
let clock_rollback_observed = observed_unix_ms < last_physical_ms;
|
||||
let physical_unix_ms = observed_unix_ms.max(last_physical_ms);
|
||||
let clock_rollback_observed = observed_time.unix_ms < last_physical_ms;
|
||||
let physical_unix_ms = observed_time.unix_ms.max(last_physical_ms);
|
||||
let logical_counter = if physical_unix_ms > last_physical_ms {
|
||||
0
|
||||
} else {
|
||||
|
|
@ -265,8 +506,11 @@ pub(crate) fn issue_at(
|
|||
physical_unix_ms,
|
||||
beijing_time,
|
||||
time_zone: "Asia/Shanghai (UTC+08:00)".into(),
|
||||
clock_source: "HOST_OPERATING_SYSTEM_REALTIME_CLOCK".into(),
|
||||
clock_verification: "LOCAL_CLOCK_NOT_NETWORK_ATTESTED".into(),
|
||||
clock_source: observed_time.clock_source.into(),
|
||||
clock_verification: observed_time.clock_verification.into(),
|
||||
network_sync_state: observed_time.network_sync_state.into(),
|
||||
network_synchronized_at_unix_ms: observed_time.network_synchronized_at_unix_ms,
|
||||
network_uncertainty_ms: observed_time.network_uncertainty_ms,
|
||||
guanghu_epoch_date: GUANGHU_EPOCH_DATE.into(),
|
||||
guanghu_calendar_state: "EPOCH_DATE_LOCKED_EXACT_INSTANT_PENDING".into(),
|
||||
elapsed_beijing_dates_since_guanghu_epoch: elapsed_dates,
|
||||
|
|
@ -377,7 +621,7 @@ fn request_digest(request: &VerifiedTicketRequest) -> String {
|
|||
fn ticket_digest(ticket: &PersonaTimeTicket) -> String {
|
||||
sha256_hex(
|
||||
format!(
|
||||
"{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}",
|
||||
"{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}\n{}",
|
||||
ticket.authority_id,
|
||||
ticket.ticket_id,
|
||||
ticket.unique_timestamp,
|
||||
|
|
@ -387,6 +631,8 @@ fn ticket_digest(ticket: &PersonaTimeTicket) -> String {
|
|||
ticket.client_instance_id,
|
||||
ticket.channel_id,
|
||||
ticket.previous_ticket_id.as_deref().unwrap_or(""),
|
||||
ticket.clock_source,
|
||||
ticket.clock_verification,
|
||||
)
|
||||
.as_bytes(),
|
||||
)
|
||||
|
|
@ -400,6 +646,139 @@ fn sha256_hex(bytes: &[u8]) -> String {
|
|||
.collect()
|
||||
}
|
||||
|
||||
fn network_anchor() -> Result<Option<NetworkClockAnchor>, String> {
|
||||
NETWORK_CLOCK_ANCHOR
|
||||
.get_or_init(|| RwLock::new(None))
|
||||
.read()
|
||||
.map(|anchor| anchor.clone())
|
||||
.map_err(|_| "HOLOLAKE_PERSONA_TIME_NETWORK_ANCHOR_UNAVAILABLE".into())
|
||||
}
|
||||
|
||||
async fn synchronize_network_time() -> Result<(), String> {
|
||||
let local_before = now_unix_ms()?;
|
||||
let client = reqwest::Client::builder()
|
||||
.redirect(reqwest::redirect::Policy::none())
|
||||
.timeout(std::time::Duration::from_secs(3))
|
||||
.build()
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_TIME_NETWORK_CLIENT_FAILED: {error}"))?;
|
||||
let response = client
|
||||
.head(NETWORK_TIME_URL)
|
||||
.header(reqwest::header::CACHE_CONTROL, "no-cache")
|
||||
.send()
|
||||
.await
|
||||
.map_err(|error| format!("HOLOLAKE_PERSONA_TIME_NETWORK_SYNC_FAILED: {error}"))?;
|
||||
let local_after = now_unix_ms()?;
|
||||
if !response.status().is_success() {
|
||||
return Err(format!(
|
||||
"HOLOLAKE_PERSONA_TIME_NETWORK_STATUS_INVALID:{}",
|
||||
response.status().as_u16()
|
||||
));
|
||||
}
|
||||
let date = response
|
||||
.headers()
|
||||
.get(reqwest::header::DATE)
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.ok_or("HOLOLAKE_PERSONA_TIME_NETWORK_DATE_MISSING")?;
|
||||
let network_unix_ms = parse_http_date_unix_ms(date)?;
|
||||
let local_midpoint = local_before.saturating_add(local_after).div_euclid(2);
|
||||
let centered_network_ms = network_unix_ms.saturating_add(500);
|
||||
let offset = i128::from(centered_network_ms) - i128::from(local_midpoint);
|
||||
let offset_ms = i64::try_from(offset)
|
||||
.map_err(|_| "HOLOLAKE_PERSONA_TIME_NETWORK_OFFSET_INVALID".to_string())?;
|
||||
let anchor = NetworkClockAnchor {
|
||||
offset_ms,
|
||||
network_unix_ms,
|
||||
uncertainty_ms: 500_u64.saturating_add(local_after.saturating_sub(local_before) / 2),
|
||||
};
|
||||
*NETWORK_CLOCK_ANCHOR
|
||||
.get_or_init(|| RwLock::new(None))
|
||||
.write()
|
||||
.map_err(|_| "HOLOLAKE_PERSONA_TIME_NETWORK_ANCHOR_UNAVAILABLE")? = Some(anchor);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn local_time_sample(unix_ms: u64) -> RealityTimeSample {
|
||||
RealityTimeSample {
|
||||
unix_ms,
|
||||
clock_source: "HOST_OPERATING_SYSTEM_REALTIME_CLOCK",
|
||||
clock_verification: "LOCAL_CLOCK_NOT_NETWORK_ATTESTED",
|
||||
network_sync_state: "NETWORK_SYNC_PENDING",
|
||||
network_synchronized_at_unix_ms: None,
|
||||
network_uncertainty_ms: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn reality_time_sample() -> Result<RealityTimeSample, String> {
|
||||
let local_unix_ms = now_unix_ms()?;
|
||||
let Some(anchor) = network_anchor()? else {
|
||||
return Ok(local_time_sample(local_unix_ms));
|
||||
};
|
||||
let adjusted = i128::from(local_unix_ms) + i128::from(anchor.offset_ms);
|
||||
let unix_ms = u64::try_from(adjusted)
|
||||
.map_err(|_| "HOLOLAKE_PERSONA_TIME_NETWORK_COORDINATE_INVALID".to_string())?;
|
||||
Ok(RealityTimeSample {
|
||||
unix_ms,
|
||||
clock_source: "HTTPS_DATE_GUANGHULAB_COM",
|
||||
clock_verification: "NETWORK_HTTPS_DATE_SYNCHRONIZED_COARSE",
|
||||
network_sync_state: "SYNCHRONIZED_ON_APPLICATION_OPEN",
|
||||
network_synchronized_at_unix_ms: Some(anchor.network_unix_ms),
|
||||
network_uncertainty_ms: Some(anchor.uncertainty_ms),
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_http_date_unix_ms(value: &str) -> Result<u64, String> {
|
||||
let parts = value.split_ascii_whitespace().collect::<Vec<_>>();
|
||||
if parts.len() != 6 || parts[5] != "GMT" || !parts[0].ends_with(',') {
|
||||
return Err("HOLOLAKE_PERSONA_TIME_NETWORK_DATE_INVALID".into());
|
||||
}
|
||||
let day = parts[1]
|
||||
.parse::<u64>()
|
||||
.map_err(|_| "HOLOLAKE_PERSONA_TIME_NETWORK_DATE_INVALID")?;
|
||||
let month = match parts[2] {
|
||||
"Jan" => 1,
|
||||
"Feb" => 2,
|
||||
"Mar" => 3,
|
||||
"Apr" => 4,
|
||||
"May" => 5,
|
||||
"Jun" => 6,
|
||||
"Jul" => 7,
|
||||
"Aug" => 8,
|
||||
"Sep" => 9,
|
||||
"Oct" => 10,
|
||||
"Nov" => 11,
|
||||
"Dec" => 12,
|
||||
_ => return Err("HOLOLAKE_PERSONA_TIME_NETWORK_DATE_INVALID".into()),
|
||||
};
|
||||
let year = parts[3]
|
||||
.parse::<i64>()
|
||||
.map_err(|_| "HOLOLAKE_PERSONA_TIME_NETWORK_DATE_INVALID")?;
|
||||
let clock = parts[4].split(':').collect::<Vec<_>>();
|
||||
if clock.len() != 3 || !(1..=31).contains(&day) || year < 1970 {
|
||||
return Err("HOLOLAKE_PERSONA_TIME_NETWORK_DATE_INVALID".into());
|
||||
}
|
||||
let hour = clock[0]
|
||||
.parse::<u64>()
|
||||
.map_err(|_| "HOLOLAKE_PERSONA_TIME_NETWORK_DATE_INVALID")?;
|
||||
let minute = clock[1]
|
||||
.parse::<u64>()
|
||||
.map_err(|_| "HOLOLAKE_PERSONA_TIME_NETWORK_DATE_INVALID")?;
|
||||
let second = clock[2]
|
||||
.parse::<u64>()
|
||||
.map_err(|_| "HOLOLAKE_PERSONA_TIME_NETWORK_DATE_INVALID")?;
|
||||
if hour > 23 || minute > 59 || second > 60 {
|
||||
return Err("HOLOLAKE_PERSONA_TIME_NETWORK_DATE_INVALID".into());
|
||||
}
|
||||
let days = days_from_civil(year, month, day);
|
||||
let seconds = u64::try_from(days)
|
||||
.map_err(|_| "HOLOLAKE_PERSONA_TIME_NETWORK_DATE_INVALID".to_string())?
|
||||
.checked_mul(86_400)
|
||||
.and_then(|value| value.checked_add(hour * 3600 + minute * 60 + second))
|
||||
.ok_or("HOLOLAKE_PERSONA_TIME_NETWORK_DATE_INVALID")?;
|
||||
seconds
|
||||
.checked_mul(1000)
|
||||
.ok_or_else(|| "HOLOLAKE_PERSONA_TIME_NETWORK_DATE_INVALID".into())
|
||||
}
|
||||
|
||||
fn now_unix_ms() -> Result<u64, String> {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
|
|
@ -407,6 +786,16 @@ fn now_unix_ms() -> Result<u64, String> {
|
|||
.map_err(|error| format!("HOLOLAKE_SYSTEM_CLOCK_INVALID: {error}"))
|
||||
}
|
||||
|
||||
fn days_from_civil(year: i64, month: u64, day: u64) -> i64 {
|
||||
let adjusted_year = year - i64::from(month <= 2);
|
||||
let era = adjusted_year.div_euclid(400);
|
||||
let year_of_era = adjusted_year - era * 400;
|
||||
let month_prime = month as i64 + if month > 2 { -3 } else { 9 };
|
||||
let day_of_year = (153 * month_prime + 2) / 5 + day as i64 - 1;
|
||||
let day_of_era = year_of_era * 365 + year_of_era / 4 - year_of_era / 100 + day_of_year;
|
||||
era * 146_097 + day_of_era - 719_468
|
||||
}
|
||||
|
||||
fn format_beijing_time(unix_ms: u64) -> Result<String, String> {
|
||||
const BEIJING_OFFSET_SECONDS: u64 = 8 * 60 * 60;
|
||||
let unix_seconds = unix_ms / 1000;
|
||||
|
|
@ -543,4 +932,31 @@ mod tests {
|
|||
1
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn https_date_header_becomes_the_same_beijing_reality_coordinate() {
|
||||
let unix_ms = parse_http_date_unix_ms("Mon, 17 Aug 2026 05:56:25 GMT").unwrap();
|
||||
assert_eq!(
|
||||
format_beijing_time(unix_ms).unwrap(),
|
||||
"2026-08-17T13:56:25.000+08:00"
|
||||
);
|
||||
assert!(parse_http_date_unix_ms("not-a-date").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn public_era_timeline_keeps_fact_boundary_and_current_coordinate() {
|
||||
let timeline = guanghu_era_timeline(1_786_947_831_456).unwrap();
|
||||
assert_eq!(timeline.era_name, "曜冥纪元");
|
||||
assert_eq!(timeline.calendar_name, "光湖历");
|
||||
assert_eq!(timeline.epoch_precision, "DAY_ONLY_EXACT_TIME_UNKNOWN");
|
||||
assert_eq!(timeline.current_coordinate.guanghu_era_day, 479);
|
||||
assert_eq!(timeline.events.len(), 12);
|
||||
assert!(timeline
|
||||
.events
|
||||
.iter()
|
||||
.all(|event| event.evidence_state == "PUBLIC_FACT_RECORD"));
|
||||
let serialized = serde_json::to_string(&timeline).unwrap();
|
||||
assert!(!serialized.contains("国家"));
|
||||
assert!(!serialized.contains("政府"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ use uuid::Uuid;
|
|||
const KERNEL_SCHEMA: &str = "hololake.personal-channel-kernel/v1";
|
||||
const DATABASE_SCHEMA_VERSION: i64 = 1;
|
||||
const ZERO_HASH: &str = "0000000000000000000000000000000000000000000000000000000000000000";
|
||||
const TIME_AUTHORITY_MODULE_ID: &str = "hololake.persona-time-authority";
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
|
|
@ -68,6 +69,19 @@ pub struct PersonalChannelEventProjection {
|
|||
pub receipt_hash: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PersonalChannelModule {
|
||||
pub module_id: String,
|
||||
pub kind: String,
|
||||
pub display_name: String,
|
||||
pub state: String,
|
||||
pub installed_at_unix_ms: i64,
|
||||
pub time_zone: String,
|
||||
pub calendar_name: String,
|
||||
pub clock_verification: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PersonalChannelIntegrity {
|
||||
|
|
@ -87,6 +101,7 @@ pub struct PersonalChannelSnapshot {
|
|||
pub identity: Option<PersonalChannelIdentity>,
|
||||
pub current_task: Option<PersonalChannelTask>,
|
||||
pub recent_events: Vec<PersonalChannelEventProjection>,
|
||||
pub modules: Vec<PersonalChannelModule>,
|
||||
pub integrity: PersonalChannelIntegrity,
|
||||
pub storage: &'static str,
|
||||
pub authority: &'static str,
|
||||
|
|
@ -204,6 +219,29 @@ fn open_database(path: &Path) -> Result<Connection, String> {
|
|||
);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS one_active_personal_task
|
||||
ON tasks(status) WHERE status = 'ACTIVE';
|
||||
CREATE TABLE IF NOT EXISTS channel_modules (
|
||||
module_id TEXT PRIMARY KEY NOT NULL,
|
||||
kind TEXT NOT NULL,
|
||||
display_name TEXT NOT NULL,
|
||||
state TEXT NOT NULL,
|
||||
installed_at_unix_ms INTEGER NOT NULL,
|
||||
time_zone TEXT NOT NULL,
|
||||
calendar_name TEXT NOT NULL,
|
||||
clock_verification TEXT NOT NULL
|
||||
);
|
||||
INSERT OR IGNORE INTO channel_modules(
|
||||
module_id, kind, display_name, state, installed_at_unix_ms,
|
||||
time_zone, calendar_name, clock_verification
|
||||
)
|
||||
SELECT 'hololake.persona-time-authority', 'PERSONA_TIME_AUTHORITY',
|
||||
'时间主控', 'READY_EVENT_TRIGGERED_TIME_AUTHORITY',
|
||||
created_at_unix_ms, 'Asia/Shanghai (UTC+08:00)', '光湖历',
|
||||
'DYNAMIC_NETWORK_SYNC_OR_EXPLICIT_LOCAL_FALLBACK'
|
||||
FROM identities WHERE singleton = 1;
|
||||
UPDATE channel_modules
|
||||
SET state = 'READY_EVENT_TRIGGERED_TIME_AUTHORITY',
|
||||
clock_verification = 'DYNAMIC_NETWORK_SYNC_OR_EXPLICIT_LOCAL_FALLBACK'
|
||||
WHERE module_id = 'hololake.persona-time-authority';
|
||||
CREATE TABLE IF NOT EXISTS events (
|
||||
sequence INTEGER PRIMARY KEY NOT NULL,
|
||||
event_id TEXT NOT NULL UNIQUE,
|
||||
|
|
@ -278,18 +316,38 @@ fn initialize_at(
|
|||
params![human_subject_id, display_name, channel_id, created_at],
|
||||
)
|
||||
.map_err(database_write_error)?;
|
||||
install_time_authority_module(&transaction, created_at)?;
|
||||
append_event(
|
||||
&transaction,
|
||||
&human_subject_id,
|
||||
"CHANNEL_INITIALIZED",
|
||||
None,
|
||||
&format!("{display_name} 建立了个人频道"),
|
||||
&format!("{display_name} 建立了个人频道,并预装时间主控"),
|
||||
created_at,
|
||||
)?;
|
||||
transaction.commit().map_err(database_write_error)?;
|
||||
snapshot_at(database)
|
||||
}
|
||||
|
||||
fn install_time_authority_module(
|
||||
transaction: &Transaction<'_>,
|
||||
installed_at_unix_ms: i64,
|
||||
) -> Result<(), String> {
|
||||
transaction
|
||||
.execute(
|
||||
"INSERT INTO channel_modules(
|
||||
module_id, kind, display_name, state, installed_at_unix_ms,
|
||||
time_zone, calendar_name, clock_verification
|
||||
) VALUES(?1, 'PERSONA_TIME_AUTHORITY', '时间主控',
|
||||
'READY_EVENT_TRIGGERED_TIME_AUTHORITY', ?2,
|
||||
'Asia/Shanghai (UTC+08:00)', '光湖历',
|
||||
'DYNAMIC_NETWORK_SYNC_OR_EXPLICIT_LOCAL_FALLBACK')",
|
||||
params![TIME_AUTHORITY_MODULE_ID, installed_at_unix_ms],
|
||||
)
|
||||
.map_err(database_write_error)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_task_at(
|
||||
database: &Path,
|
||||
input: CreatePersonalChannelTaskInput,
|
||||
|
|
@ -544,6 +602,29 @@ fn snapshot_at(database: &Path) -> Result<PersonalChannelSnapshot, String> {
|
|||
.map_err(database_read_error)?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(database_read_error)?;
|
||||
let mut module_statement = connection
|
||||
.prepare(
|
||||
"SELECT module_id, kind, display_name, state, installed_at_unix_ms,
|
||||
time_zone, calendar_name, clock_verification
|
||||
FROM channel_modules ORDER BY installed_at_unix_ms, module_id",
|
||||
)
|
||||
.map_err(database_read_error)?;
|
||||
let modules = module_statement
|
||||
.query_map([], |row| {
|
||||
Ok(PersonalChannelModule {
|
||||
module_id: row.get(0)?,
|
||||
kind: row.get(1)?,
|
||||
display_name: row.get(2)?,
|
||||
state: row.get(3)?,
|
||||
installed_at_unix_ms: row.get(4)?,
|
||||
time_zone: row.get(5)?,
|
||||
calendar_name: row.get(6)?,
|
||||
clock_verification: row.get(7)?,
|
||||
})
|
||||
})
|
||||
.map_err(database_read_error)?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(database_read_error)?;
|
||||
Ok(PersonalChannelSnapshot {
|
||||
schema: KERNEL_SCHEMA,
|
||||
state: if identity.is_some() {
|
||||
|
|
@ -554,6 +635,7 @@ fn snapshot_at(database: &Path) -> Result<PersonalChannelSnapshot, String> {
|
|||
identity,
|
||||
current_task,
|
||||
recent_events,
|
||||
modules,
|
||||
integrity,
|
||||
storage: "LOCAL_PRIVATE_SQLITE_SINGLE_HOLOLAKE_OWNER",
|
||||
authority: "LOCAL_HUMAN_CONFIRMED_IDENTITY_NOT_SERVER_AUTHORITY",
|
||||
|
|
@ -661,6 +743,16 @@ fn verify_integrity(connection: &Connection) -> Result<PersonalChannelIntegrity,
|
|||
if identity_count > 1 || (identity_count == 1 && event_count == 0) {
|
||||
return Err("HOLOLAKE_PERSONAL_CHANNEL_INTEGRITY_FAILED".into());
|
||||
}
|
||||
let time_module_count: i64 = connection
|
||||
.query_row(
|
||||
"SELECT COUNT(*) FROM channel_modules WHERE module_id = ?1",
|
||||
params![TIME_AUTHORITY_MODULE_ID],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.map_err(database_read_error)?;
|
||||
if time_module_count != identity_count {
|
||||
return Err("HOLOLAKE_PERSONAL_CHANNEL_TIME_MODULE_INTEGRITY_FAILED".into());
|
||||
}
|
||||
Ok(PersonalChannelIntegrity {
|
||||
state: "PASS_100",
|
||||
schema_version,
|
||||
|
|
@ -748,6 +840,13 @@ mod tests {
|
|||
assert_eq!(snapshot.integrity.receipt_count, 1);
|
||||
assert_eq!(snapshot.recent_events[0].kind, "CHANNEL_INITIALIZED");
|
||||
assert!(snapshot.recent_events[0].receipt_id.starts_with("HLR-"));
|
||||
assert_eq!(snapshot.modules.len(), 1);
|
||||
assert_eq!(snapshot.modules[0].module_id, TIME_AUTHORITY_MODULE_ID);
|
||||
assert_eq!(snapshot.modules[0].display_name, "时间主控");
|
||||
assert_eq!(
|
||||
snapshot.modules[0].clock_verification,
|
||||
"DYNAMIC_NETWORK_SYNC_OR_EXPLICIT_LOCAL_FALLBACK"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -774,6 +873,27 @@ mod tests {
|
|||
assert_eq!(after_restart.integrity.last_receipt_hash, before_hash);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_existing_channel_receives_the_time_module_idempotently() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let database = database(&temp);
|
||||
initialize(&database);
|
||||
let connection = open_database(&database).unwrap();
|
||||
connection
|
||||
.execute(
|
||||
"DELETE FROM channel_modules WHERE module_id = ?1",
|
||||
params![TIME_AUTHORITY_MODULE_ID],
|
||||
)
|
||||
.unwrap();
|
||||
drop(connection);
|
||||
|
||||
let migrated = snapshot_at(&database).unwrap();
|
||||
assert_eq!(migrated.modules.len(), 1);
|
||||
assert_eq!(migrated.modules[0].module_id, TIME_AUTHORITY_MODULE_ID);
|
||||
let reread = snapshot_at(&database).unwrap();
|
||||
assert_eq!(reread.modules.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_active_task_is_enforced_and_completion_is_receipted() {
|
||||
let temp = TempDir::new().unwrap();
|
||||
|
|
|
|||
Loading…
Reference in a new issue