Skip to content

Shake256 with 33 bytes #14

Open
moudyellaz wants to merge 5 commits into
mainfrom
shake256-33bytes-demo
Open

Shake256 with 33 bytes #14
moudyellaz wants to merge 5 commits into
mainfrom
shake256-33bytes-demo

Conversation

@moudyellaz
Copy link
Copy Markdown
Collaborator

@moudyellaz moudyellaz commented Aug 13, 2025

🎯 Purpose

This PR adds the same end-to-end SHAKE256 XOR-encryption demo, but with stricter typing: epk and ipk are wrapped as fixed-size 33-byte values for safer (de)serialization across the host/guest boundary.

⚙️ Approach

Identical crypto as the baseline demo:

  • NSSA-style KDF via SHA-256 with domain separation.
  • Keystream via SHAKE256 and XOR with plaintext.

The difference is in how we pass epk and ipk:

  • We define struct Bytes33(pub [u8; 33]);
  • Implement Serde manually (serialize as bytes; validate length on deserialize).
  • Implement AsRef<[u8;33]> and AsRef<[u8]> so crypto code can work with slices/arrays.
  • Use Bytes33 in EncInput on both host and guest.

Flow

  • Guest — methods/guest/src/main.rs

    • Reads EncInput { ss_bytes: [u8;32], epk_bytes: Bytes33, ipk_bytes: Bytes33, commitment: [u8;32], out_index: u32 }.
    • Builds info = epk || ipk || commitment.
    • Derives k_enc via KDF; runs SHAKE256 keystream; XORs; commits ciphertext to journal.
  • Host / Prover — src/main.rs

    • Wraps raw 33-byte arrays as Bytes33(epk_raw) / Bytes33(ipk_raw) and feeds them to the guest; proves and verifies as usual.
  • Verifier — src/main.rs

    • Calls receipt.verify(GUEST_ID).

Files

  • methods/guest/src/main.rs — includes the Bytes33 wrapper (Serde impls + AsRef), KDF + SHAKE256 XOR, entrypoint.
  • src/lib.rs — re-exports Bytes33, EncInput, and crypto helpers for the host.
  • src/main.rs — constructs Bytes33 from [u8; 33] and runs prove/verify.
  • build.rs / methods.rs — guest image binding.
  • Cargo.toml (guest) — tiny-keccak with features = ["shake"].

Notes

  • Do not derive Serialize/Deserialize on Bytes33 and also provide manual impls, pick manual impls only to avoid conflicting implementations.
    The pattern used here:

    #[derive(Clone, Copy, PartialEq, Eq, Debug)]
    pub struct Bytes33(pub [u8; 33]);
    
    impl Serialize for Bytes33 { /* serialize_bytes */ }
    impl<'de> Deserialize<'de> for Bytes33 { /* validate length = 33 */ }
    
    impl AsRef<[u8; 33]> for Bytes33 { /* … */ }
    impl AsRef<[u8]> for Bytes33 { /* … */ }
  • In the host, wrap raw arrays: Bytes33(epk_raw), Bytes33(ipk_raw).

  • Journal access: receipt.receipt.journal.bytes (field).

🧪 How to Run

From repo root:

# Build and run the 33-byte variant
cargo clean -p shake256-33bytes-demo 
cargo build -p shake256-33bytes-demo
cargo run -p shake256-33bytes-demo --release

🔗 Dependencies

Same as baseline:

tiny-keccak = { version = "2", default-features = false, features = ["shake"] }
sha2 = "0.10"
serde = { version = "1", default-features = false, features = ["alloc"] }

🔜 Future Work

  • Add tests.
  • Structure the journal (length prefix, metadata) and parse robustly.
  • Benchmark proving time; add cargo bench.
  • CI job for cargo test + example run.

📋 PR Completion Checklist

  • Complete PR description.
  • Implement Bytes33 wrapper and wire it through guest + host.
  • Implement KDF + SHAKE256 keystream + XOR.
  • Add/update documentation and inline comments.
  • Tests.

@moudyellaz moudyellaz requested review from Pravdyvy and schouhy August 13, 2025 07:49
Copy link
Copy Markdown
Contributor

@schouhy schouhy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. I thought of a few improvements to the code. I leave them as comments below.

Also, the project structure I think can be improved as follows to make the host just import a compiled version of the guest programs. Here is the diff in case you want to consider it. You'll need also to add a methods/src/lib.rs with just the content include!(concat!(env!("OUT_DIR"), "/methods.rs")); (and you can delete methods/src/main.rs probably)

serde-big-array = "0.5"
 tiny-keccak = { version = "2", default-features = false, features = ["shake"] }  # ADD
+methods = { path = "methods" }

 [dev-dependencies]
 rand = "0.8"
 cipher = { version = "0.4", features = ["std"] }
 serde = { version = "1", default-features = false, features = ["derive", "alloc"] }
-
-
-[build-dependencies]
-risc0-build = "2.3.1"
-
-[package.metadata.risc0]
-methods = ["methods/guest"]
-
-[lints.rust]
-unexpected_cfgs = { level = "allow", check-cfg = ['cfg(rust_analyzer)'] }
\ No newline at end of file
diff --git a/shake256-33bytes-demo/build.rs b/shake256-33bytes-demo/build.rs
deleted file mode 100644
index 08a8a4e..0000000
--- a/shake256-33bytes-demo/build.rs
+++ /dev/null
@@ -1,3 +0,0 @@
-fn main() {
-    risc0_build::embed_methods();
-}
diff --git a/shake256-33bytes-demo/methods/Cargo.toml b/shake256-33bytes-demo/methods/Cargo.toml
index 9ab59da..f5da0df 100644
--- a/shake256-33bytes-demo/methods/Cargo.toml
+++ b/shake256-33bytes-demo/methods/Cargo.toml
@@ -7,4 +7,4 @@ edition = "2021"
 risc0-build = { version = "2.3.1" }

 [package.metadata.risc0]
-methods = ["methods/guest"]
+methods = ["guest"]
diff --git a/shake256-33bytes-demo/src/lib.rs b/shake256-33bytes-demo/src/lib.rs
index 2bffbc1..61a26a3 100644
--- a/shake256-33bytes-demo/src/lib.rs
+++ b/shake256-33bytes-demo/src/lib.rs
@@ -1,11 +1,5 @@
 use serde::{Deserialize, Serialize};

-// ---------- expose generated guest constants ----------
-pub mod methods {
-    include!(concat!(env!("OUT_DIR"), "/methods.rs"));
-}
-// -------------------------------------------------------
-
 // ---------- 33-byte wrapper (public) ----------
 pub mod ser_bytes33 {                // (public so main.rs can use it)
     use core::fmt;
diff --git a/shake256-33bytes-demo/src/main.rs b/shake256-33bytes-demo/src/main.rs
index 50532da..0e47728 100644
--- a/shake256-33bytes-demo/src/main.rs
+++ b/shake256-33bytes-demo/src/main.rs
@@ -3,7 +3,7 @@ use risc0_zkvm::{default_prover, ExecutorEnv};

 use shake256_33bytes_demo::{EncInput, enc_xor_shake256, nssa_kdf};      // now works via re-exports
 use shake256_33bytes_demo::ser_bytes33::Bytes33;                        // for constructing wrapper
-use shake256_33bytes_demo::methods::GUEST_ELF;                          // generated guest image
+use methods::GUEST_ELF;                          // generated guest image

 fn main() -> anyhow::Result<()> {

Comment thread shake256-33bytes-demo/methods/guest/src/main.rs Outdated
Comment on lines +102 to +109
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct EncInput {
pub ss_bytes: [u8; 32],
pub epk_bytes: Bytes33,
pub ipk_bytes: Bytes33,
pub commitment: [u8; 32],
pub out_index: u32,
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a workaround to get rid of the ser_bytes33 module and the Bytes33 struct.
The issues you had with 33 bytes come from trying to derive Serialize and Deserialize for EncInput. Since you only need a way to convert an instance of EncInput to bytes, you can make it work by implementing those manually.

Suggested change
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct EncInput {
pub ss_bytes: [u8; 32],
pub epk_bytes: Bytes33,
pub ipk_bytes: Bytes33,
pub commitment: [u8; 32],
pub out_index: u32,
}
#[derive(Debug, Clone)]
pub struct EncInput {
pub ss_bytes: [u8; 32],
pub epk_bytes: [u8; 33],
pub ipk_bytes: [u8; 33],
pub commitment: [u8; 32],
pub out_index: u32,
}
impl EncInput {
pub fn to_bytes(&self) -> Vec<u8> {
let mut bytes = Vec::with_capacity(32 + 33 + 33 + 32 + 4);
bytes.extend_from_slice(&self.ss_bytes);
bytes.extend_from_slice(&self.epk_bytes);
bytes.extend_from_slice(&self.ipk_bytes);
bytes.extend_from_slice(&self.commitment);
bytes.extend_from_slice(&self.out_index.to_le_bytes());
bytes
}
fn from_bytes(bytes: Vec<u8>) -> EncInput {
assert_eq!(bytes.len(), 32 + 33 + 33 + 32 + 4);
let ss_bytes = bytes[0..32].try_into().unwrap();
let epk_bytes = bytes[32..65].try_into().unwrap();
let ipk_bytes = bytes[65..98].try_into().unwrap();
let commitment = bytes[98..130].try_into().unwrap();
let out_index = u32::from_le_bytes(bytes[130..134].try_into().unwrap());
EncInput {
ss_bytes,
epk_bytes,
ipk_bytes,
commitment,
out_index,
}
}
}

Then in src/main.rs when writing inputs to the guets program you can do the following

    let input_bytes = input.to_bytes();
    let env = ExecutorEnv::builder().write(&input_bytes.as_slice())?.build()?;

Inside the guest program, we read it as follows:

    let input_bytes: Vec<u8> = env::read();
    let EncInput { ss_bytes, epk_bytes, ipk_bytes, commitment, out_index } = EncInput::from_bytes(input_bytes);

Co-authored-by: Sergio Chouhy <41742639+schouhy@users.noreply.github.com>
@moudyellaz
Copy link
Copy Markdown
Collaborator Author

LGTM. I thought of a few improvements to the code. I leave them as comments below.

Also, the project structure I think can be improved as follows to make the host just import a compiled version of the guest programs. Here is the diff in case you want to consider it. You'll need also to add a methods/src/lib.rs with just the content include!(concat!(env!("OUT_DIR"), "/methods.rs")); (and you can delete methods/src/main.rs probably)

serde-big-array = "0.5"
 tiny-keccak = { version = "2", default-features = false, features = ["shake"] }  # ADD
+methods = { path = "methods" }

 [dev-dependencies]
 rand = "0.8"
 cipher = { version = "0.4", features = ["std"] }
 serde = { version = "1", default-features = false, features = ["derive", "alloc"] }
-
-
-[build-dependencies]
-risc0-build = "2.3.1"
-
-[package.metadata.risc0]
-methods = ["methods/guest"]
-
-[lints.rust]
-unexpected_cfgs = { level = "allow", check-cfg = ['cfg(rust_analyzer)'] }
\ No newline at end of file
diff --git a/shake256-33bytes-demo/build.rs b/shake256-33bytes-demo/build.rs
deleted file mode 100644
index 08a8a4e..0000000
--- a/shake256-33bytes-demo/build.rs
+++ /dev/null
@@ -1,3 +0,0 @@
-fn main() {
-    risc0_build::embed_methods();
-}
diff --git a/shake256-33bytes-demo/methods/Cargo.toml b/shake256-33bytes-demo/methods/Cargo.toml
index 9ab59da..f5da0df 100644
--- a/shake256-33bytes-demo/methods/Cargo.toml
+++ b/shake256-33bytes-demo/methods/Cargo.toml
@@ -7,4 +7,4 @@ edition = "2021"
 risc0-build = { version = "2.3.1" }

 [package.metadata.risc0]
-methods = ["methods/guest"]
+methods = ["guest"]
diff --git a/shake256-33bytes-demo/src/lib.rs b/shake256-33bytes-demo/src/lib.rs
index 2bffbc1..61a26a3 100644
--- a/shake256-33bytes-demo/src/lib.rs
+++ b/shake256-33bytes-demo/src/lib.rs
@@ -1,11 +1,5 @@
 use serde::{Deserialize, Serialize};

-// ---------- expose generated guest constants ----------
-pub mod methods {
-    include!(concat!(env!("OUT_DIR"), "/methods.rs"));
-}
-// -------------------------------------------------------
-
 // ---------- 33-byte wrapper (public) ----------
 pub mod ser_bytes33 {                // (public so main.rs can use it)
     use core::fmt;
diff --git a/shake256-33bytes-demo/src/main.rs b/shake256-33bytes-demo/src/main.rs
index 50532da..0e47728 100644
--- a/shake256-33bytes-demo/src/main.rs
+++ b/shake256-33bytes-demo/src/main.rs
@@ -3,7 +3,7 @@ use risc0_zkvm::{default_prover, ExecutorEnv};

 use shake256_33bytes_demo::{EncInput, enc_xor_shake256, nssa_kdf};      // now works via re-exports
 use shake256_33bytes_demo::ser_bytes33::Bytes33;                        // for constructing wrapper
-use shake256_33bytes_demo::methods::GUEST_ELF;                          // generated guest image
+use methods::GUEST_ELF;                          // generated guest image

 fn main() -> anyhow::Result<()> {

@moudyellaz moudyellaz closed this Aug 15, 2025
@moudyellaz moudyellaz reopened this Aug 15, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants