Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
40086d3
dev: added debug profile at build
irzhywau Jun 29, 2026
238e1e5
feat(dash): MPEG-DASH / CENC compliance for media assets (ELACITY-2283)
irzhywau Jul 2, 2026
939e44a
fix(runtime): DKMS quorum reliability + host-independent tests (ELACI…
irzhywau Jul 2, 2026
6798942
fix(runtime): harden DKMS lifecycle + CENC/socket safety from code re…
irzhywau Jul 2, 2026
04052ef
ci: port verify-ci/verify-capsules justfile recipes
irzhywau Jul 2, 2026
b419da2
Add the Elacity Bible: unified narrative canon for Elacity, ElastOS, …
claude Jul 2, 2026
a7c3ad6
Merge remote-tracking branch 'origin/fix/mpeg-dash-compliance' into c…
claude Jul 3, 2026
ab10be0
docs(consolidation): bank the w2 gateway consent-request patch + deci…
claude Jul 3, 2026
84ae217
restore(wasm): epoch-based operator termination lost in the mpeg-dash…
claude Jul 3, 2026
451faab
restore(media+creator): measured transcode-progress reporting lost in…
claude Jul 3, 2026
8d14230
feat(chain-provider): marketplace slice 1 — KID→ledger-tokenId resolv…
claude Jul 3, 2026
26abf3c
feat(market): slice 2 — buy/trade authorities (buy-invariant core + r…
claude Jul 3, 2026
2e8c3db
feat(market): slice 3 — /api/market/* + content-index + storefront ca…
claude Jul 3, 2026
a2dd4af
feat(market): slice 4 — library Acquire op (buy→pin) + open handoff
claude Jul 3, 2026
dd7ec4b
harden(wasm)+track(market): race-free epoch kill + register red-team …
claude Jul 3, 2026
b12528e
feat(market): slice 5 — wire the storefront into Home (routing + dev …
claude Jul 3, 2026
91ee5aa
docs(consolidation): zero-loss branch ledger — every branch → disposi…
claude Jul 3, 2026
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
106 changes: 106 additions & 0 deletions capsules/chain-provider/src/abi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,53 @@ pub(super) fn decode_asset_created_log(entry: &Value) -> Option<(String, String,
Some((operative, token_id, block_number, log_index))
}

/// From decoded `AssetCreated` candidates — each `(operative, token_id, block, log_index, tx_hash)`,
/// newest-first — plus a `tx_hash -> mint calldata input` map, return the `(operative, token_id)` of the
/// first candidate whose mint calldata BINDS the target `bytes16` KID. `AssetCreated` is the only mint
/// event that emits on Base, and it carries NO contentId — so identity is proven by the mint `opRawData`
/// (a precise canonical `decode_mint_content_id`, else the relayer-safe `mint_input_binds_content_id`
/// substring match). FAIL-CLOSED: `None` if no candidate binds the KID. Pure (no RPC) so it is
/// unit-testable; the caller scans the `AssetCreated` logs + fetches each candidate's input live.
pub(super) fn pick_asset_created_binding_kid(
decoded: &[(String, String, u64, u64, String)],
inputs: &std::collections::HashMap<String, String>,
want_content_id: &str,
) -> Option<(String, String)> {
// Gather every candidate whose mint calldata binds the KID, split by strength: PRECISE = the
// canonical `mint` decode yields exactly this contentId; SUBSTRING = the relayer-safe fallback
// (the 16 content-derived KID bytes appear in the calldata). A unique asset has a unique KID, so
// in normal operation exactly ONE candidate binds. The AssetCreated scan is NOT creator-constrained
// (topic[1] is null), so a hostile co-channel minter could embed the victim's KID in their OWN mint
// calldata; we require a UNIQUE binding and FAIL CLOSED on ambiguity (>1 distinct (operative,tokenId)
// binding the same KID) rather than bind the wrong tokenId and mis-charge the buyer. Preferring the
// canonical decode means a substring-only hostile candidate cannot displace the precise legit one.
// (Creator-constraining the scan would also let us RESOLVE the legit asset under such griefing — the
// follow-on documented in protocol.rs; this pass fails closed, which protects funds.)
let mut precise: std::collections::BTreeSet<(String, String)> = std::collections::BTreeSet::new();
let mut substring: std::collections::BTreeSet<(String, String)> = std::collections::BTreeSet::new();
for (operative, token_id, _block, _log, tx_hash) in decoded {
let Some(input) = inputs.get(tx_hash) else {
continue;
};
let is_precise = decode_mint_content_id(input)
.and_then(|cid| normalize_content_id_bytes16(&cid))
.as_deref()
== Some(want_content_id);
if is_precise {
precise.insert((operative.clone(), token_id.clone()));
} else if mint_input_binds_content_id(input, want_content_id) {
substring.insert((operative.clone(), token_id.clone()));
}
}
// Prefer the canonical decode; fall back to the substring binder only when no precise binder exists.
// In either tier a UNIQUE binding is required — otherwise fail closed.
let chosen = if precise.is_empty() { &substring } else { &precise };
match chosen.len() {
1 => chosen.iter().next().cloned(),
_ => None, // 0 binders, or an ambiguous KID binding -> fail closed (the buy must not proceed)
}
}

/// Decode the leading `bytes16` contentId (KID) from a `mint(string,uint16,bytes,bytes)`
/// calldata. `opRawData` is argument #2 (a dynamic `bytes`); its payload always begins with
/// the abi-encoded `bytes16 contentId` (left-aligned in the first word), so the first 16 bytes
Expand Down Expand Up @@ -630,4 +677,63 @@ mod tests {
assert!(!mint_input_binds_content_id("not-hex", kid));
assert!(!mint_input_binds_content_id(&relayed_hex, "deadbeef"));
}

#[test]
fn pick_asset_created_binding_kid_matches_via_mint_calldata() {
use std::collections::HashMap;
let kid = "9c2a000000000000000000000000e1a1";
let other = "00112233445566778899aabbccddeeff";
// candidate A (tokenId 7, tx 0xaa): canonical mint binds `kid`. candidate B (tokenId 9, tx 0xbb): binds `other`.
let mint_a =
encode_mint_calldata("0x47cbeeb4", "ipfs://a", 0, &encode_op_raw_free(kid).unwrap(), &[])
.unwrap();
let mint_b =
encode_mint_calldata("0x47cbeeb4", "ipfs://b", 0, &encode_op_raw_free(other).unwrap(), &[])
.unwrap();
let decoded = vec![
("0xopA".to_string(), "0x07".to_string(), 21u64, 0u64, "0xaa".to_string()),
("0xopB".to_string(), "0x09".to_string(), 20u64, 0u64, "0xbb".to_string()),
];
let mut inputs = HashMap::new();
inputs.insert("0xaa".to_string(), mint_a);
inputs.insert("0xbb".to_string(), mint_b.clone());
// Hit: the KID resolves to candidate A's (operative, tokenId), proven by the mint calldata.
assert_eq!(
pick_asset_created_binding_kid(&decoded, &inputs, kid),
Some(("0xopA".to_string(), "0x07".to_string())),
);
// Miss: an unknown KID -> None (fail closed; the buy must not proceed).
assert!(pick_asset_created_binding_kid(&decoded, &inputs, "deadbeefdeadbeefdeadbeefdeadbeef").is_none());
// Fail-closed: if the matching candidate's input is missing, it is skipped (no false bind).
let mut partial = HashMap::new();
partial.insert("0xbb".to_string(), mint_b);
assert!(pick_asset_created_binding_kid(&decoded, &partial, kid).is_none());
}

#[test]
fn pick_asset_created_binding_kid_fails_closed_on_ambiguous_binding() {
use std::collections::HashMap;
let kid = "9c2a000000000000000000000000e1a1";
// Two AssetCreated candidates on the (creator-unconstrained) channel BOTH bind the same KID
// with DIFFERENT tokenIds — the legit asset (tokenId 7) and a hostile co-channel mint that
// re-uses the victim's KID (tokenId 0x66, newest). Binding either would mis-charge the buyer,
// so the resolver must FAIL CLOSED (None) rather than pick the newest candidate.
let legit =
encode_mint_calldata("0x47cbeeb4", "ipfs://legit", 0, &encode_op_raw_free(kid).unwrap(), &[])
.unwrap();
let hostile =
encode_mint_calldata("0x47cbeeb4", "ipfs://hostile", 0, &encode_op_raw_free(kid).unwrap(), &[])
.unwrap();
let decoded = vec![
("0xopH".to_string(), "0x66".to_string(), 22u64, 0u64, "0xhh".to_string()),
("0xopA".to_string(), "0x07".to_string(), 21u64, 0u64, "0xaa".to_string()),
];
let mut inputs = HashMap::new();
inputs.insert("0xaa".to_string(), legit);
inputs.insert("0xhh".to_string(), hostile);
assert!(
pick_asset_created_binding_kid(&decoded, &inputs, kid).is_none(),
"ambiguous KID binding must fail closed, not bind the newest (hostile) tokenId",
);
}
}
145 changes: 145 additions & 0 deletions capsules/chain-provider/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,12 @@ impl ChainProvider {
&creator,
from_block.as_deref(),
),
Request::ResolveTokenId {
network,
ledger,
content_id,
from_block,
} => self.resolve_token_id(&network, &ledger, &content_id, from_block.as_deref()),
Request::AssembleCreateChannel { channel } => self.assemble_create_channel(*channel),
Request::AssembleTradeApproval {
network,
Expand Down Expand Up @@ -1296,6 +1302,145 @@ impl ChainProvider {
Ok(found)
}

/// Resolve the real ledger `tokenId` for a `bytes16` KID by scanning the channel/ledger's
/// `AssetCreated` logs (the only mint event that emits on Base) and binding the KID via each
/// candidate's mint calldata (`opRawData`) — newest-first, split-and-retry. READ-ONLY
/// (`eth_getLogs` + `eth_getTransactionByHash`); no keys. The Phase-1 buy binds THIS, never a hash
/// of the content id. Fails closed if no `AssetCreated` on the ledger binds the KID.
fn resolve_token_id(
&mut self,
network_id: &str,
ledger: &str,
content_id: &str,
from_block: Option<&str>,
) -> Response {
let network = match self.evm_network(network_id) {
Ok(network) => network.clone(),
Err(response) => return response,
};
if let Err(err) = validate_evm_address(ledger) {
return Response::error("invalid_ledger", &err);
}
let want = match normalize_content_id_bytes16(content_id) {
Some(kid) => kid,
None => return Response::error("invalid_content_id", "content_id is not a bytes16 KID"),
};
let channel_topic = match address_topic(ledger) {
Ok(topic) => topic,
Err(err) => return Response::error("invalid_ledger", &err),
};
let deploy_block = match from_block {
Some(value) => match parse_hex_u64(value.trim()) {
Ok(value) => value,
Err(err) => return Response::error("invalid_from_block", &err),
},
None => DEFAULT_CHANNEL_FROM_BLOCK,
};
let latest = match self.evm_latest_block(&network) {
Ok(latest) => latest,
Err(response) => return response,
};
let window = Self::max_log_range().max(1);
// Newest-first: an asset is minted before it can be listed, so walking down from the tip
// surfaces the match quickly and bounds RPC for the common (recent) case.
let mut to = latest;
loop {
let from = to.saturating_sub(window - 1).max(deploy_block);
match self.scan_asset_created_window_for_kid(&network, &channel_topic, &want, from, to) {
Ok(Some((operative, token_id, block))) => {
return Response::ok(json!({
"content_id": format!("0x{want}"),
"token_id": token_id,
"operative": operative,
"ledger": ledger,
"block": block,
"chain": network.id,
}));
}
Ok(None) => {}
Err(response) => return response,
}
if from <= deploy_block {
break;
}
to = from.saturating_sub(1);
}
Response::error(
"token_id_not_found",
&format!(
"no AssetCreated on ledger {ledger} whose mint binds KID 0x{want} in [{deploy_block}, {latest}]"
),
)
}

/// Scan one window of the channel's `AssetCreated` logs (any creator), fetch each candidate mint
/// tx's calldata, and return the `(operative, token_id, block)` whose mint binds the KID. Split-and-
/// retry on a range-limit error (mirrors `fetch_asset_created_logs`); the pure bind is
/// `pick_asset_created_binding_kid`.
fn scan_asset_created_window_for_kid(
&self,
network: &ChainNetwork,
channel_topic: &str,
want_kid: &str,
from: u64,
to: u64,
) -> Result<Option<(String, String, u64)>, Response> {
let filter = json!({
"fromBlock": format!("0x{from:x}"),
"toBlock": format!("0x{to:x}"),
"topics": [ASSET_CREATED_TOPIC0, Value::Null, channel_topic],
});
let logs = match self.evm_rpc_logs(network, filter) {
Ok(logs) => logs,
Err(response) => {
if Self::is_range_limit_error(&response) && to.saturating_sub(from) >= MIN_LOG_RANGE {
let mid = from + (to - from) / 2;
if let Some(hit) = self
.scan_asset_created_window_for_kid(network, channel_topic, want_kid, mid + 1, to)?
{
return Ok(Some(hit));
}
return self
.scan_asset_created_window_for_kid(network, channel_topic, want_kid, from, mid);
}
return Err(response);
}
};
let entries = logs.as_array().ok_or_else(|| {
Response::error("upstream_invalid_logs", "eth_getLogs result was not an array")
})?;
let mut decoded: Vec<(String, String, u64, u64, String)> = Vec::new();
for log in entries {
let Some((operative, token_id, block, log_index)) = decode_asset_created_log(log) else {
continue;
};
let Some(tx_hash) = log.get("transactionHash").and_then(Value::as_str) else {
continue;
};
decoded.push((operative, token_id, block, log_index, tx_hash.to_string()));
}
decoded.sort_by(|a, b| (b.2, b.3).cmp(&(a.2, a.3))); // newest-first
// Fetch each candidate's mint calldata (live), then bind the KID purely.
let mut inputs = std::collections::HashMap::new();
for (_, _, _, _, tx_hash) in &decoded {
if !inputs.contains_key(tx_hash) {
if let Some(input) = self.tx_input(network, tx_hash)? {
inputs.insert(tx_hash.clone(), input);
}
}
}
let Some((operative, token_id)) = pick_asset_created_binding_kid(&decoded, &inputs, want_kid)
else {
return Ok(None);
};
let block = decoded
.iter()
.find(|entry| entry.1 == token_id && entry.0 == operative)
.map(|entry| entry.2)
.unwrap_or(from);
Ok(Some((operative, token_id, block)))
}

/// Discover a creator's dDRM channels via a PERSISTED, RESUMABLE factory scan. Mirrors
/// PC2's `ContentIndexerService` cursor model (forward `head` + backfill `floor`), adapted
/// to the runtime's synchronous provider model: each call scans new blocks since `head`
Expand Down
18 changes: 18 additions & 0 deletions capsules/chain-provider/src/protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,24 @@ pub(super) enum Request {
#[serde(default)]
from_block: Option<String>,
},
/// Resolve the REAL on-chain ledger `tokenId` for a `bytes16` content id (KID) by scanning the
/// channel's `AssetCreated` logs (the only mint event that emits on Base; it carries NO contentId)
/// and binding the KID through each candidate's mint `opRawData` calldata — preferring the canonical
/// `mint(string,uint16,bytes,bytes)` decode over the relayer-safe substring match. READ-ONLY
/// (`eth_getLogs` + per-candidate `eth_getTransactionByHash`); no keys. The Phase-1 buy MUST bind
/// this, never `word_from_id(content_id)`. FAILS CLOSED on no match OR an AMBIGUOUS binding (>1
/// distinct tokenId binds the KID) — the scan is not yet creator-constrained, so ambiguity is
/// treated as a hostile co-channel mint and the buy is refused. `ledger` is the channel ERC-1155
/// (`metadata.properties.ledger`). Follow-on: creator-constrain the scan to also RESOLVE the legit
/// asset under such griefing (and/or use `DigitalAssetRegistered` on the channel/factory contract,
/// which carries the indexed tokenId + bytes16 contentId but does NOT emit on EventHub).
ResolveTokenId {
network: String,
ledger: String,
content_id: String,
#[serde(default)]
from_block: Option<String>,
},
/// Assemble the `createChannel(uint8,uint8,string,string,bytes)` calldata (PURE: no RPC,
/// no keys) — the `{ to, data, value }` an external signer (wallet-provider) signs and
/// `broadcast_transaction` sends to deploy a new channel. Mirrors `AssembleMint`.
Expand Down
Loading