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
Original file line number Diff line number Diff line change
Expand Up @@ -179,20 +179,13 @@ pub fn extract_operation_parameters(
amount,
token_id,
policy_commit,
authorized_by,
proof_of_authorization,
message,
} => {
let mut params = HashMap::new();
params.insert("operation_type".to_string(), b"mint".to_vec());
params.insert("amount".to_string(), balance_to_bytes(amount));
params.insert("token_id".to_string(), token_id.clone());
params.insert("policy_commit".to_string(), policy_commit.to_vec());
params.insert("authorized_by".to_string(), authorized_by.clone());
params.insert(
"proof_of_authorization".to_string(),
proof_of_authorization.clone(),
);
params.insert("message".to_string(), message.as_bytes().to_vec());
Ok(params)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -420,15 +420,12 @@ pub fn enforce_operation_authorization(operation: &Operation) -> Result<(), DsmE
));
}
}
Operation::Mint {
proof_of_authorization,
..
} => {
if proof_of_authorization.is_empty() {
return Err(DsmError::invalid_operation(
"Mint missing proof_of_authorization",
));
}
Operation::Mint { .. } => {
// Mint carries NO authorization bytes. Authorization of unit
// creation is the 0x0029 issuance evidence resolved during
// economic admission — there is nothing inside the operation for
// this legacy check to demand, and demanding anything here would
// recreate the second authorization channel that was deleted.
}
Operation::Burn {
proof_of_ownership, ..
Expand Down Expand Up @@ -1234,22 +1231,17 @@ fn apply_token_balance_delta(
}
}
Operation::Mint {
token_id,
amount,
authorized_by,
proof_of_authorization,
..
token_id, amount, ..
} => {
let token_id_str = canonical_token_id_str(token_id)
.ok_or_else(|| DsmError::invalid_operation("Mint has malformed or empty token_id"))?
.to_string();
verify_mint_authorization_for_transition(
current_state,
&token_id_str,
amount.value(),
authorized_by,
proof_of_authorization,
)?;
// No embedded-proof verification: Mint authorization is the
// 0x0029 issuance evidence, proven by the economic verifier during
// admission. This legacy path performs only the balance
// arithmetic; on the canonical device-head path the accepting
// layer refuses any positive mint without an attached admission.
let _ = amount;
let policy_commit = crate::core::token::resolve_policy_commit(&token_id_str)?;
let owner_key = crate::core::token::derive_canonical_balance_key(
&policy_commit,
Expand Down Expand Up @@ -1359,34 +1351,6 @@ fn parse_embedded_proof(proof: &[u8], label: &str) -> Result<(Vec<u8>, Vec<u8>),
Ok((pk, sig))
}

fn verify_mint_authorization_for_transition(
current_state: &State,
token_id: &str,
amount: u64,
authorized_by: &[u8],
proof: &[u8],
) -> Result<(), DsmError> {
let (pk, sig) = parse_embedded_proof(proof, "mint_proof")?;
let policy_commit = crate::core::token::resolve_policy_commit(token_id)?;

let mut msg = b"mint|v2|".to_vec();
msg.extend_from_slice(authorized_by);
msg.extend_from_slice(token_id.as_bytes());
msg.extend_from_slice(&amount.to_le_bytes());
msg.extend_from_slice(&current_state.hash);

let msg_hash = crate::crypto::blake3::token_domain_hash(&policy_commit, "mint", &msg);
let verified = crate::crypto::sphincs::sphincs_verify(&pk, msg_hash.as_bytes(), &sig)?;
if verified {
Ok(())
} else {
Err(DsmError::unauthorized(
"Invalid mint authorization proof",
None::<std::io::Error>,
))
}
}

fn verify_burn_authorization_for_transition(
current_state: &State,
token_id: &str,
Expand Down Expand Up @@ -1573,35 +1537,23 @@ mod tests {
signed_transfer_op_amount(sk, state_hash, nonce, message, "ERA", 10)
}

fn signed_mint_op_amount(sk: &[u8], token_id: &str, amount: u64) -> Operation {
let mut op = Operation::Mint {
// Mint carries no authorization bytes — authority lives in the 0x0029
// admission evidence, so a fixture mint is just the economic intent.
fn mint_op_amount(token_id: &str, amount: u64) -> Operation {
Operation::Mint {
amount: {
let mut balance = Balance::zero();
balance.update_add(amount);
balance
},
token_id: token_id.as_bytes().to_vec(),
policy_commit: [0u8; 32],
authorized_by: b"authority".to_vec(),
proof_of_authorization: vec![],
message: "test mint".to_string(),
};

let bytes = op.to_bytes();
let sig = sphincs_sign(sk, &bytes).unwrap_or_else(|e| panic!("sign mint failed: {e}"));
if let Operation::Mint {
proof_of_authorization,
..
} = &mut op
{
*proof_of_authorization = sig;
}

op
}

fn signed_mint_op(sk: &[u8]) -> Operation {
signed_mint_op_amount(sk, "token2", 100)
fn signed_mint_op(_sk: &[u8]) -> Operation {
mint_op_amount("token2", 100)
}

fn signed_burn_op_amount(sk: &[u8], token_id: &str, amount: u64) -> Operation {
Expand Down Expand Up @@ -1899,8 +1851,7 @@ mod tests {
balance.update_add(150);
balance
});
let (_state, _pk, sk) = create_test_state_with_keypair(0);
let mint_op = signed_mint_op_amount(&sk, "token1", 50);
let mint_op = mint_op_amount("token1", 50);

let result = verify_token_balance_consistency(&prev_state, &current_state, &mint_op);
assert!(result.is_ok());
Expand Down Expand Up @@ -2209,8 +2160,7 @@ mod tests {
let current_state = create_test_state(2);

// Mint operation but token not added to current state
let (_state, _pk, sk) = create_test_state_with_keypair(0);
let mint_op = signed_mint_op_amount(&sk, "new_token", 100);
let mint_op = mint_op_amount("new_token", 100);

let result = verify_token_balance_consistency(&prev_state, &current_state, &mint_op);
assert!(result.is_ok());
Expand All @@ -2236,8 +2186,7 @@ mod tests {
balance
});

let (_state, _pk, sk) = create_test_state_with_keypair(0);
let mint_op = signed_mint_op_amount(&sk, "token1", 100);
let mint_op = mint_op_amount("token1", 100);

let result = verify_token_balance_consistency(&prev_state, &current_state, &mint_op);
assert!(result.is_ok());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -366,12 +366,16 @@ impl PolicyEnforcer {
}

PolicyCondition::TokenAuthority { signers, threshold } => {
// Only gates value issuance/destruction; other operations are
// governed by their own conditions.
if !matches!(
ctx.operation_type.as_str(),
"mint" | "burn" | "create_token"
) {
// Gates burn and create_token, which still authorize through
// the embedded `token_authorization_preimage` witness. MINT IS
// DELIBERATELY EXCLUDED: since the 0x0029 producer cut, mint
// authorization is the policy-signed issuance evidence bundle
// verified during economic admission — the operation carries
// no witness for this condition to check, and gating it here
// would resurrect the second authorization channel that was
// deleted. Other operations are governed by their own
// conditions.
if !matches!(ctx.operation_type.as_str(), "burn" | "create_token") {
return Ok(EnforcementResult::allowed(
"TokenAuthority does not gate this operation",
tick,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -335,27 +335,13 @@ impl TokenStateManager {
}

Operation::Mint {
amount,
token_id,
authorized_by,
proof_of_authorization,
..
amount, token_id, ..
} => {
let token_id_str = canonical_token_id_str(token_id).ok_or_else(|| {
DsmError::invalid_operation("Mint has malformed or empty token_id")
})?;
if !self.verify_mint_authorization(
token_id_str,
authorized_by,
amount.value(),
current_state.hash,
proof_of_authorization,
)? {
return Err(DsmError::unauthorized(
"Invalid mint authorization",
None::<std::io::Error>,
));
}
// No embedded-proof verification: Mint authorization is the
// 0x0029 issuance evidence, proven during economic admission.

let owner_pk = &current_state.device_info.public_key;
let owner_key = self.make_balance_key(owner_pk, token_id_str)?;
Expand Down Expand Up @@ -483,59 +469,6 @@ impl TokenStateManager {
))
}

fn verify_mint_authorization(
&self,
token_id: &str,
authorized_by: &[u8],
amount: u64,
state_hash: [u8; 32],
proof: &[u8],
) -> Result<bool, DsmError> {
if proof.is_empty() {
return Ok(false);
}

// proof := u16 pk_len | pk_bytes | u16 sig_len | sig_bytes
if proof.len() < 4 {
return Ok(false);
}

let mut idx: usize = 0;
let read_u16 = |buf: &[u8], i: &mut usize| -> Result<u16, DsmError> {
if *i + 2 > buf.len() {
return Err(DsmError::invalid_parameter(
"mint_proof: truncated length field",
));
}
let v = u16::from_le_bytes([buf[*i], buf[*i + 1]]);
*i += 2;
Ok(v)
};
let read_bytes = |buf: &[u8], i: &mut usize, n: usize| -> Result<Vec<u8>, DsmError> {
if *i + n > buf.len() {
return Err(DsmError::invalid_parameter("mint_proof: truncated field"));
}
let out = buf[*i..*i + n].to_vec();
*i += n;
Ok(out)
};

let pk_len = read_u16(proof, &mut idx)? as usize;
let pk = read_bytes(proof, &mut idx, pk_len)?;
let sig_len = read_u16(proof, &mut idx)? as usize;
let sig = read_bytes(proof, &mut idx, sig_len)?;

let policy_commit = self.resolve_policy_commit(token_id)?;
let mut msg = b"mint|v2|".to_vec();
msg.extend_from_slice(authorized_by);
msg.extend_from_slice(token_id.as_bytes());
msg.extend_from_slice(&amount.to_le_bytes());
msg.extend_from_slice(&state_hash);
let msg_hash = crate::crypto::blake3::token_domain_hash(&policy_commit, "mint", &msg);

sphincs::sphincs_verify(&pk, msg_hash.as_bytes(), &sig)
}

fn verify_token_ownership(
&self,
token_id: &str,
Expand Down Expand Up @@ -628,11 +561,10 @@ impl TokenStateManager {
);
context.insert("recipient".to_string(), recipient.clone());
}
Operation::Mint {
amount,
authorized_by,
..
} => {
Operation::Mint { amount, .. } => {
// Amount facts stay (supply semantics may read them); the
// legacy authorized_by witness is gone — Mint authority is the
// 0x0029 evidence, not a caller-chosen byte string.
context.insert(
"amount_u64".to_string(),
amount.value().to_le_bytes().to_vec(),
Expand All @@ -641,7 +573,6 @@ impl TokenStateManager {
"amount".to_string(),
amount.value().to_string().into_bytes(),
);
context.insert("authorized_by".to_string(), authorized_by.clone());
}
Operation::Burn { amount, .. } => {
context.insert(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,11 @@ use crate::ccb::{class, push_digest32, push_envelope, push_u32, push_u64, CcbErr

/// `0x0023` schema 1 — funded by an authorized issuance transition.
///
/// The authorization itself is addressed rather than inline: its class
/// (`0x0029`) is still **reserved**, because a field table for it would encode
/// an issuance predicate this protocol does not yet define. Referencing it by
/// address costs nothing today and commits nothing prematurely.
/// The authorization itself is addressed rather than inline: class `0x0029`
/// (`IssuanceAuthorizationBody`) defines the issuance predicate, and the
/// descriptor names the evidence bundle carrying it by INNER content identity.
/// Inlining the bundle here would put one fact in two encodings; the arm
/// fetches and re-verifies the addressed bytes instead.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CreditSourceAuthorizedIssuance {
pub credit_mutation_index: u32,
Expand Down
Loading
Loading