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
41 changes: 0 additions & 41 deletions dsm_client/android/app/src/main/dsm_env_config.toml

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -1625,7 +1625,7 @@ class MainActivity : AppCompatActivity(), NfcAdapter.ReaderCallback {
val outFile = File(files, assetName)

// Materialize bundled assets that the SDK reads from filesDir.
// ca.crt is the self-signed CA for AWS storage node TLS certs.
// ca.crt is the storage fleet's CA, the one the bundled env config names.
materializeAssetIfBundled("ca.crt", files)

fun firstExisting(vararg candidates: File?): File? = candidates.firstOrNull { it != null && it.exists() }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ impl NetworkConfigLoader {
}

/// Parse TOML using serde for type safety and automatic deserialization.
fn parse_env_config_toml(toml_str: &str) -> Result<EnvConfig, DsmError> {
pub(crate) fn parse_env_config_toml(toml_str: &str) -> Result<EnvConfig, DsmError> {
let mut config: EnvConfig = toml::from_str(toml_str).map_err(|e| {
DsmError::serialization_error(
"STRICT: failed to parse TOML env config",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -698,7 +698,31 @@ impl SetClient {
#[cfg(test)]
#[allow(clippy::disallowed_methods)]
mod tests {
use super::{build_ca_aware_client, read_ca_certs, LatestByteCommit, MemberClient};
use super::{
build_ca_aware_client, build_client_from, read_ca_certs, CaMaterial, LatestByteCommit,
MemberClient,
};

/// The env config the app bundles names the CA bundled beside it, and the
/// storage client builds from that CA through the same reader and builder
/// every client is made by. (Which CA issued the fleet's certificates is
/// not decidable offline; a device run against the fleet is what shows the
/// bundled CA verifies them.)
#[test]
fn the_bundled_env_config_names_a_ca_the_storage_client_builds_from() {
let path = concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../frontend/public/dsm_env_config.toml"
);
let certs = read_ca_certs(path).expect("the bundled CA is named and readable");
assert_eq!(certs.len(), 1, "exactly the fleet's CA");
assert!(certs[0].0.ends_with("ca.crt"));
build_client_from(&CaMaterial {
env_path: Some(path.to_string()),
certs,
})
.expect("the storage client builds from the bundled CA");
}

/// A write the member did not take is not acknowledged. The same running
/// node, reached at a path it does not serve, answers `404`: the put, the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,13 @@ impl StorageSetCatalog {
/// where `name` is the node's configured protocol identity (`node.id`) and
/// `endpoint` its transport address.
pub fn from_env_config() -> Result<Self, DsmError> {
let env = crate::network::NetworkConfigLoader::load_env_config()?;
Self::from_env(crate::network::NetworkConfigLoader::load_env_config()?)
}

/// The catalog a parsed env config states — the one [`Self::from_env_config`]
/// reads from the configured file, and the one the env config bundled into
/// the app states.
pub(crate) fn from_env(env: crate::network::EnvConfig) -> Result<Self, DsmError> {
let members: Vec<StorageMember> = env
.nodes
.into_iter()
Expand Down Expand Up @@ -245,14 +251,28 @@ impl StorageSetCatalog {
/// Resolve the canonical register set for `network_id`, fail-closed, through
/// the catalog (never `sole_set` — consumers RESOLVE).
pub fn canonical_set(network_id: &[u8]) -> Result<StorageSet, DsmError> {
let profile =
dsm::economic::register::resolve_root_register_profile(network_id).map_err(|e| {
DsmError::storage(
format!("root register profile: {e}"),
None::<std::io::Error>,
)
})?;
let catalog = StorageSetCatalog::from_env_config()?;
let profile = root_register_profile(network_id)?;
canonical_set_in(&profile, &StorageSetCatalog::from_env_config()?)
}

/// The network's pinned root-register profile, fail-closed.
fn root_register_profile(
network_id: &[u8],
) -> Result<dsm::economic::register::RootRegisterProfile, DsmError> {
dsm::economic::register::resolve_root_register_profile(network_id).map_err(|e| {
DsmError::storage(
format!("root register profile: {e}"),
None::<std::io::Error>,
)
})
}

/// The set of `catalog` that re-derives `profile`'s pinned id, or the
/// fail-closed refusal.
fn canonical_set_in(
profile: &dsm::economic::register::RootRegisterProfile,
catalog: &StorageSetCatalog,
) -> Result<StorageSet, DsmError> {
// The set id is a function of `(member_id, register_incarnation_id)`
// pairs, so it cannot be asked for by name: the catalog offers candidates
// and `verify_candidate` refuses any that does not re-derive the pinned id.
Expand Down Expand Up @@ -313,6 +333,30 @@ mod tests {
);
}

/// The env config the app bundles is what a default install reaches the
/// network with, so its `(member, incarnation)` pairs must be the pinned
/// beta set: a stale pair (a retired fleet, an incarnation from before a
/// reprovision) makes every read of the canonical set fail closed on a
/// fresh device, with nothing on screen but the intro.
#[test]
fn the_bundled_env_config_resolves_the_pinned_beta_set() {
let path = concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../frontend/public/dsm_env_config.toml"
);
let text = std::fs::read_to_string(path).expect("the bundled env config");
let env = crate::network::parse_env_config_toml(&text)
.expect("the bundled env config parses under the loader");
let catalog = StorageSetCatalog::from_env(env).expect("its members form one set");
let profile = root_register_profile(b"dsm-testnet").expect("the beta profile");
let set = canonical_set_in(&profile, &catalog)
.expect("the bundled env config resolves the pinned beta set");
assert_eq!(
crate::util::text_id::encode_base32_crockford(&set.id()),
"7GBBB51DM8XP433F6H896G4R88W6RJZATZ3CT0WTRAFZ2EHYZ9D0"
);
}

#[test]
fn beta_root_register_pins_render_to_the_logged_values() {
let logged = [
Expand Down
2 changes: 1 addition & 1 deletion dsm_client/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"proto:gen": "npx protoc --plugin=protoc-gen-es=./node_modules/.bin/protoc-gen-es --es_out=import_extension=.js:./src/proto --es_opt=target=ts -I ../../proto ../../proto/dsm_app.proto",
"copy:android": "node scripts/copy-to-android.js",
"build:android": "cd ../android && ./gradlew assembleDebug",
"build:full-deploy": "npm run build:android-webpack && npm run copy:android",
"build:full-deploy": "npm run build:android-webpack && npm run copy:android && npm run validate:android-assets",
"clean:android": "rimraf ../android/app/src/main/assets/*",
"test:device": "./test-device.sh",
"build:nuclear": "node scripts/build-and-deploy-nuclear.js",
Expand Down
54 changes: 27 additions & 27 deletions dsm_client/frontend/public/ca.crt
Original file line number Diff line number Diff line change
@@ -1,31 +1,31 @@
-----BEGIN CERTIFICATE-----
MIIFSTCCAzGgAwIBAgIURsiHUZIdwJgdYSZy1dBcNF5dV0gwDQYJKoZIhvcNAQEL
MIIFSTCCAzGgAwIBAgIUOeBVAVMR2zmh4EtFCeJYmocSQ68wDQYJKoZIhvcNAQEL
BQAwNDELMAkGA1UEBhMCWFgxDDAKBgNVBAoMA0RTTTEXMBUGA1UEAwwORFNNLVN0
b3JhZ2UtQ0EwHhcNMjYwNzA0MTUzODI2WhcNMzYwNzAxMTUzODI2WjA0MQswCQYD
b3JhZ2UtQ0EwHhcNMjYwOTEyMDM1NTAwWhcNMzYwOTA5MDM1NTAwWjA0MQswCQYD
VQQGEwJYWDEMMAoGA1UECgwDRFNNMRcwFQYDVQQDDA5EU00tU3RvcmFnZS1DQTCC
AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAJj7zGFmO4T/f8yf8Zscg5u+
PZgH6QE6dnFmbrXXPuT+Gu6qfrcccAsTd55V9pG+xTRIPEXdak09RIf8MWdKX2BC
kf9orsqy9iXa0fb8RlBnR7NIG/cytv1ICFHYfRPlYimf5uWNqlnppGpBP5Fnkwty
XPUHJlBIw0yoAvZCIV4Ve/7g8lZsg7fhVsQitttBH0GZU91Q7aZCADwVSKx4bbBi
+gazgrfc+e1Eg7xtNs9wnwcL/Qi00VtGw0bAMn6LnpGBACvL2fjhbFKrYSrpixLF
9RvKwPQEK7AYUJpMz937twO12ogstkrLF2+8yXlDLWfYook+BFjpydP7F1C0m9xV
l7zqkBDZrqhqDT4C1Qg8zQiri+0VMpjo3HKwLhfo5HV17SwxR7Z7nY5qtcoI0YSW
s0IDk8V+jrRyi39W97XYyHFnNxdpEG+VTx8Te13NBra5cYBu99ojGk+T8zY+khpI
XwWgfjsbIGOfm5Hqd9wgVT4ZqZGRPeoUjzVhzMIGiid/lKU2jkTbZqQwC3r0fBcJ
8/TTDq8c6C/1rBbo7ifrBXP/zeYZlvHs0v+NISaLkA6xdtoElJjWIL9Xzr2Mz+vm
YODHOoXWzDhsR3z+tPn8xFrtTMcnQkoFeoRLm4csC6/MhqsmoC8biVUeJvZ1/rjo
bJ0yxeyBJUlBdnCLzn7NAgMBAAGjUzBRMB0GA1UdDgQWBBSgUZ8P3Ia5N5YyIZxo
/4ht553DqjAfBgNVHSMEGDAWgBSgUZ8P3Ia5N5YyIZxo/4ht553DqjAPBgNVHRMB
Af8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4ICAQBNq1iJt/16HL0EmINj3ey0W8tb
fS7TvuKSJOJw430NmOgfh4U8NiRm0y8gURztJiwKfpidiZZzYKiuQk2sTKH1WZv+
EB0eDPFU0NRJCK6wPWHt/5lU38tfLtep0fFOY0ZfX4/cdafP32WQol2Ihi9GqDlm
fJVI8AL/F0Tx2jb+oAcb4WOhv20Q4hmpK2m0eDly/cvu6slRC4NLojGftJ0KXeph
ehBHwsn+uqclrkHWXti4DJjTHrYEY16p7mCVfz6FpQGqYom4dJjmK3wO5pCD2d8R
4FrkalDWJT6DFSvDR2wfcId0W7ZlKwRTTCjc2kIYXR0SFB4lSsbLE4WRTKrAnVZX
WiNIKMkxTMoEqeoF7v6gyyy/r2e5eeo8ZZTuYOaSTXmM60qCcWwtd0G1VvN7DODk
zW9H6RmS1FWm3WPPUgMU7pYu4Z9P6gCW1dkQjs/NNr4qmf47PbjvQXikGe4OeNMT
txFxbqTKwpPdfnGvj1H/QHBN9XbcWl1B+/WNKLGRT7tRnwdZ8HRMss7sS449yrXR
L9Fxex2b9f3dyJKgxDus+Gsqtpy1iq/iWEIwtt88QXJ629mI+a+a4xn+IUkBugnj
YE2KOj3xslUvoWOnD4JI5ZDrh22TneZpahdfEDJu9AiPELJTBXQZZEgwKfpn+UGW
zX0igzm3udg5ZeYJqQ==
AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAJkIYRepQO/Q/VnpCJqnxtsA
kGGRqJh48ehyAfPgWfH1Dv9A/o283qQqZSc+DdEKueArz5e0mqJKdC1/mxxmMX7E
2iGN4+BrgpKrntSRatuxdY6Xq/heFjmErCiuTpAtnwP22S7jeOxzdXefpEfN4vlF
gmpGXbrTvfRU/ELAAPgxTgEz0XTvps/ma4uZlHaB3HX8dw8+w6keeeyimVBYoxLz
Nu/LRLBQGkxd/Uo8kUF4dDs+8hI83J5paLYeIHHAsngFEnR0NybnMNVDlEKOmYmF
z45U1/f6qHFF5XANaaJ1x/hD/0hcIhbTCYhA3vHJvp/oIYTtWd2o6JYqlse0TfjY
39vQ8h9XXP/MJQ0hCcb3dZL38DxJn7UMfMOjrIFwFg66cnGIVlKL4qCaSdSpDMDQ
GBjOa7kZvl/CcMfmPBdRtpTVbzipTdfs95S79UdAumdtIOlwen+wkKr75N1ShARi
2VvNm9ufhU1bDBREM6OMFk3rSMXRus8RtknJ+J9JPmbZPQDHgHXTtQn0pE6m6xRu
8Uyu0csZYc4tHcOoVqsCrDDoyMcSHqIc6l9dqq1A5ce8rUNIpPf172lDchHZze4A
yqcAEtxBMFJMA6kSNqM+Cn7NniO2CQWStDd4SG3EDLIYbhZo4UcS47fNDQRQl5ja
fGzCvqfG0owKShKKVRSpAgMBAAGjUzBRMB0GA1UdDgQWBBTg35BVePyiQ3JV0ImJ
2e8VEFx9GDAfBgNVHSMEGDAWgBTg35BVePyiQ3JV0ImJ2e8VEFx9GDAPBgNVHRMB
Af8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4ICAQAnIshWgVnk1T9EKWOKK1QVUki2
BrY18NxSUNoQHGhNIECL0ZzlUOIY+loVTv5sHCWZJbcvaOHhIdHUhAmSnG2iFG+z
Ae37NaLy0SwlGXeYxFskZoGcbgtDyikCiw24XJ/J7v1qvnXopMavKGColurH7OeB
76s4a5SH4i0pDc6JzSTOd6GPXl7NqDxufd8o9fVc/SWIEjTcJ4HailHtq3cv1bRN
Nv14v5LH1//aQpcfceRdWeAZHFeRCRwlB4cQL5Cs/nKvEKEhNDTxgc1qrL9uOMd+
nEfiMXkFWa6OGbsZFkoGWEqmMbOrJheBDHwQTzs6r/Wz2Vt1OIrfxOS3+V+OKeEy
9E1dmctUOOdhgBaYchWwFXlr/mZcpefVNrVqY0pyPb7kV1P5qeOn7P/21fRYbnCH
+SNF7/dUHwvVmd+2R1aDRP5agBnZytex+T8RaQ3b8rvRD5ewjG6tgQ9TuzZ8Q1dK
+hbNa+FXoaKQbPfGH6IafKuQEuW08Um8Dl2Ut+T91TtZnGnU400Z39cHBdOXDUZW
SFzTOm0w2cIj4vFXEI/cRq2VaxBWfUAEANVvzHPOy2cpRldrBcyOKSIsQI0cNjYE
iWL9McPQJe5RpaKfQfCEWNT17E2v/YbkAGq2rkeYvGJ3t7ObNTx64jAckmstKu9Z
z5YOPTWcM5j1FJxV0w==
-----END CERTIFICATE-----
47 changes: 35 additions & 12 deletions dsm_client/frontend/public/dsm_env_config.toml
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
# DSM environment config — Alibaba Cloud storage nodes (3-node, us-west-1)
# Push to device with: scripts/push_env_override.sh --alibaba
# DSM environment config — the beta storage fleet (5 nodes, GCP us-central1).
# Bundled into the APK: this file and ca.crt beside it are what a default
# install reaches the network with.
#
# Nodes (ECS, VPC vpc-rj9oepiel1c2517avaboj, sg dsm-storage-sg):
# us-west-1a: 47.251.246.93, 47.251.250.159, 47.251.88.58
# The fleet was reprovisioned 2026-09-27 on the storage node at main f779cb872;
# every member minted a new register incarnation into its fresh database, and
# the committed set id every member logs is
# 7GBBB51DM8XP433F6H896G4R88W6RJZATZ3CT0WTRAFZ2EHYZ9D0
# which is the id core pins (dsm::economic::register), so this list must stay
# exactly the pinned pairs: `dsm_sdk::sdk::storage_set::tests::
# the_bundled_env_config_resolves_the_pinned_beta_set` refuses anything else.

allow_localhost = false

bitcoin_network = "signet"
dbtc_min_confirmations = 1
mempool_api_url = "https://mempool.space"

# Self-signed CA for Alibaba storage node TLS certs
# Path is relative to this config file's directory (resolved by SDK).
# The fleet's CA (CN=DSM-Storage-CA): every member serves a certificate for
# its IP and CN=dsm-node-N issued by it. Path is relative to this config
# file's directory (resolved by the SDK); the app installs the bundled ca.crt
# next to the config.
custom_ca_certs = ["ca.crt"]

# `name` IS THE MEMBER ID OF THE CANONICAL STORAGE SET, not a label. It is
Expand All @@ -20,17 +28,32 @@ custom_ca_certs = ["ca.crt"]
# toward quorum when the node echoes this exact string back as x-dsm-node-id.
#
# So each name MUST equal that node's own `[node] id`, which is also the CN of
# the TLS certificate it serves. A name that merely describes the node
# ("us-west-1a-1") makes the client compute a set id no node belongs to: every
# settlement-slot claim is refused as a foreign set.
# the TLS certificate it serves. `register_incarnation` is the register history
# that member serves, as its own database minted it at first boot and logged
# it; the set id is derived from the (name, register_incarnation) pairs, so a
# stale incarnation makes the client compute a set id no member belongs to and
# every read of the canonical set fails closed.
[[nodes]]
name = "dsm-node-1"
endpoint = "https://47.251.246.93:8080"
endpoint = "https://34.58.75.224:8080"
register_incarnation = "B6DZ4TFJ2Y1GSJ8X57CE8BWX0QM5JRVAJP08JRQTV65DEQ53DYKG"

[[nodes]]
name = "dsm-node-2"
endpoint = "https://47.251.250.159:8080"
endpoint = "https://35.254.209.202:8080"
register_incarnation = "QYR6K65CD8SZ40G4PZV4PMS2R0VK02N14TECZ0GP00ZR9VCSYC20"

[[nodes]]
name = "dsm-node-3"
endpoint = "https://47.251.88.58:8080"
endpoint = "https://146.148.99.141:8080"
register_incarnation = "2AJ8GZ4QM7YH7D5G552EAHSPTSTWG9KXE04RTBKYN8ES17Y9BTWG"

[[nodes]]
name = "dsm-node-4"
endpoint = "https://104.197.96.66:8080"
register_incarnation = "S95VHW0YZ413DA4YX972ZA9XDA69FXF4KBCG2AHG89DA8PTY200G"

[[nodes]]
name = "dsm-node-5"
endpoint = "https://35.184.46.44:8080"
register_incarnation = "56VB5TY2G8QKVJNVFKJQX9N0GHJN6VEQ1KV9E88GX272V8G43RWG"
33 changes: 27 additions & 6 deletions dsm_client/frontend/scripts/copy-to-android.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,12 @@ if (!fs.existsSync(targetDir)) {
console.log('Created Android assets directory');
}

// Remove stale assets (but preserve whitelisted files such as env configs)
const whitelist = new Set(['dsm_env_config.toml', 'ca.crt']);
console.log('Cleaning existing Android assets (preserving whitelist)...');
// The Android assets directory is regenerated on every build: nothing in it
// is kept from a previous build, so nothing stale can be packaged.
console.log('Cleaning existing Android assets...');
try {
const existing = fs.readdirSync(targetDir);
existing.forEach((name) => {
if (whitelist.has(name)) return; // keep whitelisted files
const full = path.join(targetDir, name);
try {
const stat = fs.lstatSync(full);
Expand Down Expand Up @@ -90,5 +89,27 @@ if (fs.existsSync(overlayDir)) {
copyRecursive(overlayDir, targetDir);
}

// Single source of truth for config: android/app/src/main/assets/dsm_env_config.toml.
// Do NOT add code that copies over that file — it will break allow_localhost.
// The network config and the fleet's CA the app ships with. Their one tracked
// source is frontend/public/; they are written here straight from it, after
// everything else, and checked byte for byte. They are not taken from dist/:
// a build once packaged dist/'s previous copy of both while public/ held the
// new ones, and the app then trusted a retired CA and a retired fleet. For a
// local fleet, push an override config to the device
// (scripts/push_env_override.sh); do not edit the copies here — every build
// rewrites them.
const RUNTIME_CONFIG = ['dsm_env_config.toml', 'ca.crt'];
const publicDir = path.join(__dirname, '..', 'public');
for (const name of RUNTIME_CONFIG) {
const source = path.join(publicDir, name);
const target = path.join(targetDir, name);
if (!fs.existsSync(source)) {
console.error(`Error: ${source} is missing; the app cannot reach the network without it`);
process.exit(1);
}
fs.copyFileSync(source, target);
if (!fs.readFileSync(source).equals(fs.readFileSync(target))) {
console.error(`Error: ${target} does not equal ${source} after copying`);
process.exit(1);
}
console.log(`OK: ${name} written from public/`);
}
Loading
Loading