0.1.0
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
// file: kb_decoder_spl_elgamal_registry/src/constants.rs
|
||||
// version: 2
|
||||
|
||||
//! Constants for the SPL ElGamal registry decoder.
|
||||
|
||||
pub(crate) const SURFACE_CODE: &str = "spl_elgamal_registry";
|
||||
pub(crate) const EVENT_VERSION: u32 = 1;
|
||||
pub(crate) const MAX_INSTRUCTION_BYTES: usize = 16;
|
||||
|
||||
/// Canonical tracing target for this crate.
|
||||
pub(crate) const TRACING_TARGET: &str = "kb_decoder_spl_elgamal_registry";
|
||||
@@ -0,0 +1,256 @@
|
||||
// file: kb_decoder_spl_elgamal_registry/src/decoder.rs
|
||||
// version: 6
|
||||
|
||||
//! Exact dispatch for the SPL ElGamal registry program.
|
||||
|
||||
const SURFACES: &[kb_decoder_api::DecoderSurface] = &[kb_decoder_api::DecoderSurface {
|
||||
program_id: kb_program_ids::SPL_TOKEN_2022_ELGAMAL_REGISTRY_PROGRAM_ID,
|
||||
surface_code: crate::SURFACE_CODE,
|
||||
priority: 100,
|
||||
}];
|
||||
|
||||
const PROGRAM_IDS: &[&str] = &[kb_program_ids::SPL_TOKEN_2022_ELGAMAL_REGISTRY_PROGRAM_ID];
|
||||
|
||||
/// Decoder for the dedicated Token-2022 ElGamal public-key registry program.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct SplElgamalRegistryDecoder;
|
||||
|
||||
impl kb_decoder_api::ProtocolDecoder for crate::SplElgamalRegistryDecoder {
|
||||
fn decoder_name(&self) -> &'static str {
|
||||
return "kb_decoder_spl_elgamal_registry";
|
||||
}
|
||||
|
||||
fn decoder_version(&self) -> &'static str {
|
||||
return env!("CARGO_PKG_VERSION");
|
||||
}
|
||||
|
||||
fn program_ids(&self) -> &'static [&'static str] {
|
||||
return PROGRAM_IDS;
|
||||
}
|
||||
|
||||
fn supports_observation(
|
||||
&self,
|
||||
observation: &kb_model::ProgramObservation,
|
||||
) -> kb_decoder_api::DecoderSupport {
|
||||
return if kb_decoder_api::ProtocolDecoder::handles_program_id(self, &observation.program_id)
|
||||
{
|
||||
kb_decoder_api::DecoderSupport::Yes
|
||||
} else {
|
||||
kb_decoder_api::DecoderSupport::No
|
||||
};
|
||||
}
|
||||
|
||||
fn decode_observation(
|
||||
&self,
|
||||
_observation: &kb_model::ProgramObservation,
|
||||
) -> kb_core::Result<std::vec::Vec<kb_model::DecodedProtocolEvent>> {
|
||||
return std::result::Result::Ok(std::vec::Vec::new());
|
||||
}
|
||||
}
|
||||
|
||||
impl kb_decoder_api::InstructionDecoder for crate::SplElgamalRegistryDecoder {
|
||||
fn identity(&self) -> kb_decoder_api::DecoderIdentity {
|
||||
return kb_decoder_api::DecoderIdentity {
|
||||
name: "spl_elgamal_registry".to_string(),
|
||||
version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
fn surfaces(&self) -> &'static [kb_decoder_api::DecoderSurface] {
|
||||
return SURFACES;
|
||||
}
|
||||
|
||||
fn coverage(&self) -> std::vec::Vec<kb_decoder_api::DecoderCoverageDeclaration> {
|
||||
return [(0_u8, "create_registry"), (1_u8, "update_registry")]
|
||||
.into_iter()
|
||||
.map(|(tag, code)| {
|
||||
return kb_decoder_api::DecoderCoverageDeclaration {
|
||||
program_id: kb_program_ids::SPL_TOKEN_2022_ELGAMAL_REGISTRY_PROGRAM_ID
|
||||
.to_string(),
|
||||
surface_code: std::option::Option::Some(crate::SURFACE_CODE.to_string()),
|
||||
entry_kind: kb_decoder_api::DecoderCoverageEntryKind::Instruction,
|
||||
entry_code: code.to_string(),
|
||||
discriminator_hex: std::option::Option::Some(format!("{tag:02x}")),
|
||||
historical: false,
|
||||
};
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
|
||||
fn recognize(
|
||||
&self,
|
||||
input: &kb_store_core::CoreInstructionReplayInput,
|
||||
) -> kb_decoder_api::DecoderRecognition {
|
||||
if input.program_id != kb_program_ids::SPL_TOKEN_2022_ELGAMAL_REGISTRY_PROGRAM_ID {
|
||||
return kb_decoder_api::DecoderRecognition::incompatible();
|
||||
}
|
||||
let entry = input
|
||||
.instruction_payload_json
|
||||
.as_ref()
|
||||
.and_then(|payload| return payload.get("dataBase64"))
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.and_then(|encoded| {
|
||||
use base64::Engine; // rust-rules: trait-import
|
||||
return base64::engine::general_purpose::STANDARD.decode(encoded.as_bytes()).ok();
|
||||
})
|
||||
.and_then(|bytes| return bytes.first().copied())
|
||||
.and_then(|tag| {
|
||||
return match tag {
|
||||
0 => std::option::Option::Some("create_registry".to_string()),
|
||||
1 => std::option::Option::Some("update_registry".to_string()),
|
||||
_ => std::option::Option::None,
|
||||
};
|
||||
});
|
||||
return kb_decoder_api::DecoderRecognition::compatible(
|
||||
entry.is_some(),
|
||||
100,
|
||||
std::option::Option::Some(crate::SURFACE_CODE.to_string()),
|
||||
entry,
|
||||
std::option::Option::None,
|
||||
);
|
||||
}
|
||||
|
||||
fn decode(
|
||||
&self,
|
||||
input: &kb_store_core::CoreInstructionReplayInput,
|
||||
) -> kb_decoder_api::DecoderExecutionResult {
|
||||
if input.program_id != kb_program_ids::SPL_TOKEN_2022_ELGAMAL_REGISTRY_PROGRAM_ID {
|
||||
return kb_decoder_api::DecoderExecutionResult::unsupported(std::option::Option::None);
|
||||
}
|
||||
let result = crate::decode(input);
|
||||
if matches!(
|
||||
result.status,
|
||||
kb_decoder_api::DecoderOutcomeStatus::Failed
|
||||
| kb_decoder_api::DecoderOutcomeStatus::Unsupported
|
||||
) {
|
||||
tracing::error!(
|
||||
target: crate::TRACING_TARGET,
|
||||
action = "decode_failure",
|
||||
signature = %input.signature,
|
||||
slot = input.slot,
|
||||
instruction_path = %input.instruction_path,
|
||||
program_id = %input.program_id,
|
||||
processor_name = "spl_elgamal_registry",
|
||||
processor_version = env!("CARGO_PKG_VERSION"),
|
||||
input_key = %input.replay_input_key,
|
||||
payload_hash = ?input.instruction_payload_hash,
|
||||
transaction_failed = input.transaction_failed,
|
||||
result_status = ?result.status,
|
||||
diagnostics = ?result.diagnostics,
|
||||
"SPL ElGamal registry instruction was not decoded successfully"
|
||||
);
|
||||
}
|
||||
tracing::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
action = "decode",
|
||||
signature = %input.signature,
|
||||
instruction_path = %input.instruction_path,
|
||||
program_id = %input.program_id,
|
||||
transaction_failed = input.transaction_failed,
|
||||
result_status = ?result.status,
|
||||
observation_count = result.observations.len(),
|
||||
"SPL ElGamal registry instruction decode completed"
|
||||
);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use base64::Engine; // rust-rules: trait-import
|
||||
|
||||
fn input(
|
||||
program_id: &str,
|
||||
payload: &[u8],
|
||||
failed: bool,
|
||||
path: &str,
|
||||
) -> kb_store_core::CoreInstructionReplayInput {
|
||||
let result = kb_store_core::CoreInstructionReplayInput::new(
|
||||
format!("signature:{path}"),
|
||||
"signature",
|
||||
42,
|
||||
path,
|
||||
program_id,
|
||||
failed,
|
||||
if failed {
|
||||
std::option::Option::Some(serde_json::json!({"InstructionError":[0,"Custom"]}))
|
||||
} else {
|
||||
std::option::Option::None
|
||||
},
|
||||
serde_json::json!([]),
|
||||
serde_json::json!([]),
|
||||
std::option::Option::Some(serde_json::json!({
|
||||
"dataBase64": base64::engine::general_purpose::STANDARD.encode(payload)
|
||||
})),
|
||||
std::option::Option::None,
|
||||
serde_json::json!([]),
|
||||
serde_json::json!([]),
|
||||
serde_json::json!([]),
|
||||
serde_json::json!([]),
|
||||
);
|
||||
return match result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("registry replay input failed: {error}"),
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exact_program_boundary_and_interface_id_are_preserved() {
|
||||
assert_eq!(
|
||||
spl_elgamal_registry_interface::id().to_string(),
|
||||
kb_program_ids::SPL_TOKEN_2022_ELGAMAL_REGISTRY_PROGRAM_ID
|
||||
);
|
||||
let decoder = crate::SplElgamalRegistryDecoder;
|
||||
assert_eq!(kb_decoder_api::InstructionDecoder::coverage(&decoder).len(), 2);
|
||||
let foreign = input(kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID, &[0, 1], false, "0");
|
||||
assert_eq!(
|
||||
kb_decoder_api::InstructionDecoder::decode(&decoder, &foreign).status,
|
||||
kb_decoder_api::DecoderOutcomeStatus::Unsupported
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_and_update_decode_exact_offsets_and_commit_state() {
|
||||
for (tag, code, offset, failed, path) in [
|
||||
(0_u8, "create_registry", 1_i8, false, "0"),
|
||||
(1_u8, "update_registry", -1_i8, true, "1/0"),
|
||||
(1_u8, "update_registry", 0_i8, false, "2"),
|
||||
] {
|
||||
let input = input(
|
||||
kb_program_ids::SPL_TOKEN_2022_ELGAMAL_REGISTRY_PROGRAM_ID,
|
||||
&[tag, offset as u8],
|
||||
failed,
|
||||
path,
|
||||
);
|
||||
let result = kb_decoder_api::InstructionDecoder::decode(
|
||||
&crate::SplElgamalRegistryDecoder,
|
||||
&input,
|
||||
);
|
||||
assert_eq!(result.status, kb_decoder_api::DecoderOutcomeStatus::Decoded);
|
||||
assert_eq!(result.recognized_entry_code.as_deref(), std::option::Option::Some(code));
|
||||
assert_eq!(
|
||||
result.observations[0].payload_json["proofInstructionOffset"],
|
||||
serde_json::json!(offset)
|
||||
);
|
||||
assert_eq!(result.observations[0].observation_committed, !failed);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malformed_unknown_and_suffixed_wire_fail_closed() {
|
||||
for payload in [std::vec::Vec::new(), vec![0], vec![2, 0], vec![0, 1, 2]] {
|
||||
let input = input(
|
||||
kb_program_ids::SPL_TOKEN_2022_ELGAMAL_REGISTRY_PROGRAM_ID,
|
||||
payload.as_slice(),
|
||||
false,
|
||||
"0",
|
||||
);
|
||||
let result = kb_decoder_api::InstructionDecoder::decode(
|
||||
&crate::SplElgamalRegistryDecoder,
|
||||
&input,
|
||||
);
|
||||
assert_eq!(result.status, kb_decoder_api::DecoderOutcomeStatus::Failed);
|
||||
assert!(result.observations.is_empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
// file: kb_decoder_spl_elgamal_registry/src/lib.rs
|
||||
// version: 5
|
||||
|
||||
//! Decoder for the SPL Token-2022 ElGamal public-key registry program.
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
mod constants;
|
||||
mod decoder;
|
||||
mod registry;
|
||||
pub mod state;
|
||||
|
||||
/// Crate-root access to `EVENT_VERSION` from `constants`.
|
||||
pub(crate) use crate::constants::EVENT_VERSION;
|
||||
/// Crate-root access to `MAX_INSTRUCTION_BYTES` from `constants`.
|
||||
pub(crate) use crate::constants::MAX_INSTRUCTION_BYTES;
|
||||
/// Crate-root access to `SURFACE_CODE` from `constants`.
|
||||
pub(crate) use crate::constants::SURFACE_CODE;
|
||||
/// Canonical tracing target for this crate.
|
||||
pub(crate) use crate::constants::TRACING_TARGET;
|
||||
/// Crate-root access to `ParsedRegistryInstruction` from `registry`.
|
||||
pub(crate) use crate::registry::ParsedRegistryInstruction;
|
||||
/// Crate-root access to `decode` from `registry`.
|
||||
pub(crate) use crate::registry::decode;
|
||||
/// Crate-root access to `decode_payload` from `registry`.
|
||||
pub(crate) use crate::registry::decode_payload;
|
||||
/// Crate-root access to `parse` from `registry`.
|
||||
pub(crate) use crate::registry::parse;
|
||||
|
||||
/// Exact decoder for the SPL ElGamal registry program.
|
||||
pub use crate::decoder::SplElgamalRegistryDecoder;
|
||||
/// Exact byte length of one SPL ElGamal registry account.
|
||||
pub use crate::state::ELGAMAL_REGISTRY_ACCOUNT_LEN;
|
||||
/// Crate-root access to `ElGamalRegistryState` from `state`.
|
||||
pub use crate::state::ElGamalRegistryState;
|
||||
/// Parse one exact SPL ElGamal registry account.
|
||||
pub use crate::state::parse_elgamal_registry_state;
|
||||
@@ -0,0 +1,191 @@
|
||||
// file: kb_decoder_spl_elgamal_registry/src/registry.rs
|
||||
// version: 3
|
||||
|
||||
//! Exact two-byte SPL ElGamal registry instruction decoding.
|
||||
|
||||
use base64::Engine; // rust-rules: trait-import
|
||||
use sha2::Digest; // rust-rules: trait-import
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct ParsedRegistryInstruction {
|
||||
pub(crate) code: &'static str,
|
||||
pub(crate) tag: u8,
|
||||
pub(crate) proof_instruction_offset: i8,
|
||||
}
|
||||
|
||||
pub(crate) fn decode_payload(
|
||||
input: &kb_store_core::CoreInstructionReplayInput,
|
||||
) -> std::result::Result<std::vec::Vec<u8>, std::string::String> {
|
||||
let payload = match input.instruction_payload_json.as_ref() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(
|
||||
"ElGamal registry instruction payload is not retained".to_string(),
|
||||
);
|
||||
},
|
||||
};
|
||||
let encoded = match payload.get("dataBase64").and_then(serde_json::Value::as_str) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(
|
||||
"ElGamal registry payload does not contain dataBase64".to_string(),
|
||||
);
|
||||
},
|
||||
};
|
||||
if encoded.len() > 32 {
|
||||
return std::result::Result::Err(
|
||||
"ElGamal registry base64 payload is too large".to_string(),
|
||||
);
|
||||
}
|
||||
let bytes = match base64::engine::general_purpose::STANDARD.decode(encoded.as_bytes()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => {
|
||||
return std::result::Result::Err(format!(
|
||||
"ElGamal registry payload is not valid base64: {error}"
|
||||
));
|
||||
},
|
||||
};
|
||||
if bytes.len() > crate::MAX_INSTRUCTION_BYTES {
|
||||
return std::result::Result::Err(format!(
|
||||
"ElGamal registry payload exceeds {} bytes",
|
||||
crate::MAX_INSTRUCTION_BYTES
|
||||
));
|
||||
}
|
||||
return std::result::Result::Ok(bytes);
|
||||
}
|
||||
|
||||
pub(crate) fn parse(
|
||||
bytes: &[u8],
|
||||
) -> std::result::Result<crate::ParsedRegistryInstruction, std::string::String> {
|
||||
if bytes.len() != 2 {
|
||||
return std::result::Result::Err(format!(
|
||||
"ElGamal registry instruction requires exactly 2 bytes; received {}",
|
||||
bytes.len()
|
||||
));
|
||||
}
|
||||
let code = match bytes[0] {
|
||||
0 => "create_registry",
|
||||
1 => "update_registry",
|
||||
tag => return std::result::Result::Err(format!("unknown ElGamal registry tag {tag}")),
|
||||
};
|
||||
return std::result::Result::Ok(crate::ParsedRegistryInstruction {
|
||||
code,
|
||||
tag: bytes[0],
|
||||
proof_instruction_offset: bytes[1] as i8,
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) fn decode(
|
||||
input: &kb_store_core::CoreInstructionReplayInput,
|
||||
) -> kb_decoder_api::DecoderExecutionResult {
|
||||
let bytes = match crate::decode_payload(input) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(message) => {
|
||||
return kb_decoder_api::DecoderExecutionResult {
|
||||
status: kb_decoder_api::DecoderOutcomeStatus::Failed,
|
||||
recognized_entry_code: std::option::Option::None,
|
||||
observations: std::vec::Vec::new(),
|
||||
diagnostics: std::vec![kb_decoder_api::DecoderDiagnostic {
|
||||
code: "elgamal_registry_payload_invalid".to_string(),
|
||||
message,
|
||||
retriable: false,
|
||||
}],
|
||||
};
|
||||
},
|
||||
};
|
||||
let parsed = match crate::parse(bytes.as_slice()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(message) => {
|
||||
return kb_decoder_api::DecoderExecutionResult {
|
||||
status: kb_decoder_api::DecoderOutcomeStatus::Failed,
|
||||
recognized_entry_code: bytes.first().and_then(|tag| {
|
||||
return match tag {
|
||||
0 => std::option::Option::Some("create_registry".to_string()),
|
||||
1 => std::option::Option::Some("update_registry".to_string()),
|
||||
_ => std::option::Option::None,
|
||||
};
|
||||
}),
|
||||
observations: std::vec::Vec::new(),
|
||||
diagnostics: std::vec![kb_decoder_api::DecoderDiagnostic {
|
||||
code: "elgamal_registry_wire_invalid".to_string(),
|
||||
message,
|
||||
retriable: false,
|
||||
}],
|
||||
};
|
||||
},
|
||||
};
|
||||
let committed = !input.transaction_failed;
|
||||
let wire_hash = hash(bytes.as_slice());
|
||||
let event = kb_model::DecodedProtocolEvent {
|
||||
signature: kb_model::Signature(input.signature.clone()),
|
||||
slot: kb_model::Slot(input.slot),
|
||||
instruction_path: kb_model::InstructionPath(input.instruction_path.clone()),
|
||||
program_id: kb_model::ProgramId(input.program_id.clone()),
|
||||
protocol_code: kb_model::ProtocolCode(crate::SURFACE_CODE.to_string()),
|
||||
surface_code: kb_model::SurfaceCode(crate::SURFACE_CODE.to_string()),
|
||||
event_code: kb_model::EventCode(format!("{}.{}", crate::SURFACE_CODE, parsed.code)),
|
||||
event_name: kb_model::EventName(parsed.code.to_string()),
|
||||
event_family: kb_model::EventFamily::Admin,
|
||||
source_kind: if input.instruction_path.contains('/') {
|
||||
kb_model::EventSourceKind::InnerInstruction
|
||||
} else {
|
||||
kb_model::EventSourceKind::Instruction
|
||||
},
|
||||
confidence: kb_model::DecoderConfidence::ManualExact,
|
||||
};
|
||||
let observation = kb_decoder_api::DecodedObservation {
|
||||
event_key: format!("elgamal_registry:{}:0", parsed.code),
|
||||
event,
|
||||
payload_json: serde_json::json!({
|
||||
"eventVersion": crate::EVENT_VERSION,
|
||||
"instruction": parsed.code,
|
||||
"wireTag": parsed.tag,
|
||||
"proofInstructionOffset": parsed.proof_instruction_offset,
|
||||
"proofLocationKind": if parsed.proof_instruction_offset == 0 { "context_state_account" } else { "relative_instruction_offset" },
|
||||
"instructionPath": input.instruction_path,
|
||||
"instructionLocation": if input.instruction_path.contains('/') { "inner" } else { "outer" },
|
||||
"transactionSucceeded": !input.transaction_failed,
|
||||
"committed": committed,
|
||||
"accounts": input.instruction_accounts_json,
|
||||
"wire": {
|
||||
"lengthBytes": bytes.len(),
|
||||
"sha256": wire_hash,
|
||||
"prefixHex": hex(bytes.as_slice()),
|
||||
"complete": true,
|
||||
},
|
||||
"programBoundary": {
|
||||
"registryProgramId": kb_program_ids::SPL_TOKEN_2022_ELGAMAL_REGISTRY_PROGRAM_ID,
|
||||
"token2022ProgramId": kb_program_ids::SPL_TOKEN_2022_PROGRAM_ID,
|
||||
"zkProofProgramId": kb_program_ids::ZK_ELGAMAL_PROOF_PROGRAM_ID,
|
||||
"proofDecodedHere": false,
|
||||
}
|
||||
}),
|
||||
transaction_failed: input.transaction_failed,
|
||||
transaction_error: input.transaction_err_json.clone(),
|
||||
observation_committed: committed,
|
||||
proof: kb_decoder_api::DecoderProof {
|
||||
kind: kb_decoder_api::DecoderProofKind::Manual,
|
||||
confidence: kb_model::DecoderConfidence::ManualExact,
|
||||
evidence: std::vec![
|
||||
"spl_elgamal_registry_interface_0_2_1_registry_instruction_unpack".to_string(),
|
||||
format!("wire_tag:{}", parsed.tag),
|
||||
format!("wire_sha256:{wire_hash}"),
|
||||
],
|
||||
},
|
||||
};
|
||||
return kb_decoder_api::DecoderExecutionResult {
|
||||
status: kb_decoder_api::DecoderOutcomeStatus::Decoded,
|
||||
recognized_entry_code: std::option::Option::Some(parsed.code.to_string()),
|
||||
observations: std::vec![observation],
|
||||
diagnostics: std::vec::Vec::new(),
|
||||
};
|
||||
}
|
||||
|
||||
fn hash(bytes: &[u8]) -> std::string::String {
|
||||
let digest = sha2::Sha256::digest(bytes);
|
||||
return digest.iter().map(|byte| return format!("{byte:02x}")).collect();
|
||||
}
|
||||
|
||||
fn hex(bytes: &[u8]) -> std::string::String {
|
||||
return bytes.iter().map(|byte| return format!("{byte:02x}")).collect();
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
// file: kb_decoder_spl_elgamal_registry/src/state.rs
|
||||
// version: 2
|
||||
|
||||
//! Strict SPL ElGamal registry account-state parsing.
|
||||
|
||||
use base64::Engine; // rust-rules: trait-import
|
||||
|
||||
/// Exact byte length of one SPL ElGamal registry account.
|
||||
pub const ELGAMAL_REGISTRY_ACCOUNT_LEN: usize = 64;
|
||||
|
||||
/// Public state retained by one SPL ElGamal registry account.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct ElGamalRegistryState {
|
||||
/// Wallet address associated with this registry.
|
||||
pub owner: String,
|
||||
/// Exact 32-byte ElGamal public key encoded as standard base64.
|
||||
pub elgamal_pubkey_base64: String,
|
||||
/// Exact account bytes encoded as lowercase hexadecimal.
|
||||
pub wire_hex: String,
|
||||
}
|
||||
|
||||
/// Parse one exact SPL ElGamal registry account.
|
||||
pub fn parse_elgamal_registry_state(
|
||||
data: &[u8],
|
||||
) -> std::result::Result<ElGamalRegistryState, std::string::String> {
|
||||
if data.len() != ELGAMAL_REGISTRY_ACCOUNT_LEN {
|
||||
return std::result::Result::Err(format!(
|
||||
"SPL ElGamal registry account requires exactly {ELGAMAL_REGISTRY_ACCOUNT_LEN} bytes; received {}",
|
||||
data.len()
|
||||
));
|
||||
}
|
||||
return std::result::Result::Ok(ElGamalRegistryState {
|
||||
owner: bs58::encode(&data[..32]).into_string(),
|
||||
elgamal_pubkey_base64: base64::engine::general_purpose::STANDARD.encode(&data[32..64]),
|
||||
wire_hex: lower_hex(data),
|
||||
});
|
||||
}
|
||||
|
||||
fn lower_hex(bytes: &[u8]) -> String {
|
||||
let mut output = String::with_capacity(bytes.len() * 2);
|
||||
for byte in bytes {
|
||||
output.push_str(&format!("{byte:02x}"));
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use base64::Engine; // rust-rules: trait-import
|
||||
|
||||
#[test]
|
||||
fn exact_registry_state_preserves_owner_key_and_wire() {
|
||||
let mut data = [0u8; crate::ELGAMAL_REGISTRY_ACCOUNT_LEN];
|
||||
data[..32].fill(7);
|
||||
data[32..].fill(9);
|
||||
let parsed = crate::parse_elgamal_registry_state(&data);
|
||||
assert_eq!(
|
||||
parsed.as_ref().map(|state| return state.owner.clone()),
|
||||
std::result::Result::Ok(bs58::encode([7u8; 32]).into_string())
|
||||
);
|
||||
assert_eq!(
|
||||
parsed.as_ref().map(|state| return state.elgamal_pubkey_base64.clone()),
|
||||
std::result::Result::Ok(base64::engine::general_purpose::STANDARD.encode([9u8; 32]))
|
||||
);
|
||||
assert_eq!(
|
||||
parsed.as_ref().map(|state| return state.wire_hex.len()),
|
||||
std::result::Result::Ok(128)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn registry_state_rejects_every_non_exact_length() {
|
||||
assert!(crate::parse_elgamal_registry_state(&[0; 63]).is_err());
|
||||
assert!(crate::parse_elgamal_registry_state(&[0; 65]).is_err());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user