diff --git a/Cargo.toml b/Cargo.toml
index 8fda2ddd69feabc99c84fd4cbfeb19b3ec1c294e..6e3c6597652955e21c40416340addf866640f292 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "komodo"
-version = "0.1.0"
+version = "0.2.0"
 edition = "2021"
 
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
diff --git a/benches/commit.rs b/benches/commit.rs
index 178ede8a9e7f45b68dc3efbc93545b2ee4a1c88f..446d14dc275ea066edd086321eeed6299e3c8435 100644
--- a/benches/commit.rs
+++ b/benches/commit.rs
@@ -19,7 +19,7 @@ where
 {
     let rng = &mut rand::thread_rng();
 
-    let setup = zk::setup::<_, F, G>(degree, rng).unwrap();
+    let setup = zk::setup::<F, G>(degree, rng).unwrap();
     let polynomial = P::rand(degree, rng);
 
     c.bench_function(&format!("commit (komodo) {} on {}", degree, curve), |b| {
diff --git a/benches/recoding.rs b/benches/recoding.rs
index 7eb3f56937384e6667f6dcd2e1c763bba94db6c6..499c9bdd28df098ed8dcb356368c33d5b06af85c 100644
--- a/benches/recoding.rs
+++ b/benches/recoding.rs
@@ -6,7 +6,7 @@ use ark_std::rand::Rng;
 use criterion::{criterion_group, criterion_main, Criterion};
 
 use komodo::{
-    fec::{combine, Shard},
+    fec::{recode_with_coeffs, Shard},
     field,
 };
 
@@ -50,7 +50,7 @@ fn bench_template<F: PrimeField>(
             "recoding {} bytes and {} shards with k = {} on {}",
             nb_bytes, nb_shards, k, curve
         ),
-        |b| b.iter(|| combine(&shards, &coeffs)),
+        |b| b.iter(|| recode_with_coeffs(&shards, &coeffs)),
     );
 }
 
diff --git a/benches/setup.rs b/benches/setup.rs
index 1a4dcffe02bc39630a4b5f62f0b2d002e1331fb6..23226081f459e142f6d378fcc3d7b486665444af 100644
--- a/benches/setup.rs
+++ b/benches/setup.rs
@@ -21,7 +21,7 @@ where
     let rng = &mut rand::thread_rng();
 
     c.bench_function(&format!("setup (komodo) {} on {}", degree, curve), |b| {
-        b.iter(|| zk::setup::<_, F, G>(degree, rng).unwrap())
+        b.iter(|| zk::setup::<F, G>(degree, rng).unwrap())
     });
 }
 
@@ -60,7 +60,7 @@ where
 
     let rng = &mut rand::thread_rng();
 
-    let setup = zk::setup::<_, F, G>(degree, rng).unwrap();
+    let setup = zk::setup::<F, G>(degree, rng).unwrap();
 
     group.bench_function(
         &format!("serializing with compression {} on {}", degree, curve),
diff --git a/examples/benches/commit.rs b/examples/benches/commit.rs
index 733b362272fa5d3a340c5d62f43a7320ea489917..111fce114b6092de22e81daea534bb517546cfa6 100644
--- a/examples/benches/commit.rs
+++ b/examples/benches/commit.rs
@@ -20,7 +20,7 @@ where
     let max_degree = *degrees.iter().max().unwrap_or(&0);
 
     eprint!("building trusted setup for degree {}... ", max_degree);
-    let setup = zk::setup::<_, F, G>(max_degree, rng).unwrap();
+    let setup = zk::setup::<F, G>(max_degree, rng).unwrap();
     eprintln!("done");
 
     for (i, degree) in degrees.iter().enumerate() {
diff --git a/examples/benches/setup_size.rs b/examples/benches/setup_size.rs
index 34304c8e25c81f9cce6b1c11d13f91cab95deac4..1513f39b19603fd46f25dfe69ef64ce86e09beb6 100644
--- a/examples/benches/setup_size.rs
+++ b/examples/benches/setup_size.rs
@@ -17,7 +17,7 @@ where
 
     eprintln!("degree: {}", degree);
 
-    let setup = zk::setup::<_, F, G>(degree, rng).unwrap();
+    let setup = zk::setup::<F, G>(degree, rng).unwrap();
 
     for compress in [Compress::Yes, Compress::No] {
         println!(
diff --git a/src/error.rs b/src/error.rs
index 997eba45293a5779e2aebc3b9e90b0c194c86ae1..a5f01141a23e88c86324d3a580a0fc117599d8b6 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -15,6 +15,8 @@ pub enum KomodoError {
     IncompatibleMatrixShapes(usize, usize, usize, usize),
     #[error("Expected at least {1} shards, got {0}")]
     TooFewShards(usize, usize),
+    #[error("Shards are incompatible: {0}")]
+    IncompatibleShards(String),
     #[error("Blocks are incompatible: {0}")]
     IncompatibleBlocks(String),
     #[error("Degree is zero")]
diff --git a/src/fec.rs b/src/fec.rs
index 5dda8c378271aa2715f204768a190b5fb12e913b..47993e668131c0054af4433de9e188601387e1a1 100644
--- a/src/fec.rs
+++ b/src/fec.rs
@@ -2,6 +2,7 @@
 
 use ark_ff::PrimeField;
 use ark_serialize::{CanonicalDeserialize, CanonicalSerialize};
+use ark_std::rand::RngCore;
 
 use rs_merkle::{algorithms::Sha256, Hasher};
 
@@ -25,7 +26,7 @@ pub struct Shard<F: PrimeField> {
 
 impl<F: PrimeField> Shard<F> {
     /// compute the linear combination between two [`Shard`]s
-    pub fn combine(&self, alpha: F, other: &Self, beta: F) -> Self {
+    pub fn recode_with(&self, alpha: F, other: &Self, beta: F) -> Self {
         if alpha.is_zero() {
             return other.clone();
         } else if beta.is_zero() {
@@ -55,11 +56,11 @@ impl<F: PrimeField> Shard<F> {
 /// compute the linear combination between an arbitrary number of [`Shard`]s
 ///
 /// > **Note**
-/// > this is basically a multi-[`Shard`] wrapper around [`Shard::combine`]
+/// > this is basically a multi-[`Shard`] wrapper around [`Shard::recode_with`]
 /// >
 /// > returns [`None`] if number of shards is not the same as the number of
 /// > coefficients or if no shards are provided.
-pub fn combine<F: PrimeField>(shards: &[Shard<F>], coeffs: &[F]) -> Option<Shard<F>> {
+pub fn recode_with_coeffs<F: PrimeField>(shards: &[Shard<F>], coeffs: &[F]) -> Option<Shard<F>> {
     if shards.len() != coeffs.len() {
         return None;
     }
@@ -72,11 +73,49 @@ pub fn combine<F: PrimeField>(shards: &[Shard<F>], coeffs: &[F]) -> Option<Shard
         .zip(coeffs)
         .skip(1)
         .fold((shards[0].clone(), coeffs[0]), |(acc_s, acc_c), (s, c)| {
-            (acc_s.combine(acc_c, s, *c), F::one())
+            (acc_s.recode_with(acc_c, s, *c), F::one())
         });
     Some(s)
 }
 
+/// compute a recoded shard from an arbitrary set of shards
+///
+/// coefficients will be drawn at random, one for each shard.
+///
+/// if the shards appear to come from different data, e.g. if `k` is not the
+/// same or the hash of the data is different, an error will be returned.
+///
+/// > **Note**
+/// > this is a wrapper around [`recode_with_coeffs`].
+pub fn recode_random<F: PrimeField>(
+    shards: &[Shard<F>],
+    rng: &mut impl RngCore,
+) -> Result<Option<Shard<F>>, KomodoError> {
+    for (i, (s1, s2)) in shards.iter().zip(shards.iter().skip(1)).enumerate() {
+        if s1.k != s2.k {
+            return Err(KomodoError::IncompatibleShards(format!(
+                "k is not the same at {}: {} vs {}",
+                i, s1.k, s2.k
+            )));
+        }
+        if s1.hash != s2.hash {
+            return Err(KomodoError::IncompatibleShards(format!(
+                "hash is not the same at {}: {:?} vs {:?}",
+                i, s1.hash, s2.hash
+            )));
+        }
+        if s1.size != s2.size {
+            return Err(KomodoError::IncompatibleShards(format!(
+                "size is not the same at {}: {} vs {}",
+                i, s1.size, s2.size
+            )));
+        }
+    }
+
+    let coeffs = shards.iter().map(|_| F::rand(rng)).collect::<Vec<_>>();
+    Ok(recode_with_coeffs(shards, &coeffs))
+}
+
 /// applies a given encoding matrix to some data to generate encoded shards
 ///
 /// > **Note**
@@ -91,7 +130,7 @@ pub fn encode<F: PrimeField>(
     let k = encoding_mat.height;
 
     let source_shards = Matrix::from_vec_vec(
-        field::split_data_into_field_elements::<F>(data, k)
+        field::split_data_into_field_elements(data, k)
             .chunks(k)
             .map(|c| c.to_vec())
             .collect(),
@@ -146,7 +185,7 @@ pub fn decode<F: PrimeField>(shards: Vec<Shard<F>>) -> Result<Vec<u8>, KomodoErr
 
     let source_shards = encoding_mat.invert()?.mul(&shard_mat)?.transpose().elements;
 
-    let mut bytes = field::merge_elements_into_bytes::<F>(&source_shards);
+    let mut bytes = field::merge_elements_into_bytes(&source_shards);
     bytes.resize(shards[0].size, 0);
     Ok(bytes)
 }
@@ -162,7 +201,7 @@ mod tests {
         linalg::Matrix,
     };
 
-    use super::combine;
+    use super::recode_with_coeffs;
 
     fn bytes() -> Vec<u8> {
         include_bytes!("../tests/dragoon_32x32.png").to_vec()
@@ -188,8 +227,8 @@ mod tests {
         let mut rng = ark_std::test_rng();
 
         let mut shards = encode(data, &Matrix::random(k, n, &mut rng)).unwrap();
-        shards[1] = shards[2].combine(to_curve::<F>(7), &shards[4], to_curve::<F>(6));
-        shards[2] = shards[1].combine(to_curve::<F>(5), &shards[3], to_curve::<F>(4));
+        shards[1] = shards[2].recode_with(to_curve(7), &shards[4], to_curve(6));
+        shards[2] = shards[1].recode_with(to_curve(5), &shards[3], to_curve(4));
         assert_eq!(
             data,
             decode::<F>(shards).unwrap(),
@@ -232,7 +271,7 @@ mod tests {
             k: 2,
             linear_combination: linear_combination.to_vec(),
             hash: vec![],
-            data: field::split_data_into_field_elements::<F>(bytes, 1),
+            data: field::split_data_into_field_elements(bytes, 1),
             size: 0,
         }
     }
@@ -241,16 +280,16 @@ mod tests {
         let a: Shard<F> = create_fake_shard(&[F::one(), F::zero()], &[1, 2, 3]);
         let b: Shard<F> = create_fake_shard(&[F::zero(), F::one()], &[4, 5, 6]);
 
-        let c = a.combine(to_curve::<F>(3), &b, to_curve::<F>(5));
+        let c = a.recode_with(to_curve(3), &b, to_curve(5));
 
         assert_eq!(
             c,
-            create_fake_shard(&[to_curve::<F>(3), to_curve::<F>(5),], &[23, 31, 39])
+            create_fake_shard(&[to_curve(3), to_curve(5),], &[23, 31, 39])
         );
 
         assert_eq!(
-            c.combine(to_curve::<F>(2), &a, to_curve::<F>(4),),
-            create_fake_shard(&[to_curve::<F>(10), to_curve::<F>(10),], &[50, 70, 90],)
+            c.recode_with(to_curve(2), &a, to_curve(4),),
+            create_fake_shard(&[to_curve(10), to_curve(10),], &[50, 70, 90],)
         );
     }
 
@@ -260,23 +299,20 @@ mod tests {
     }
 
     fn combine_shards_template<F: PrimeField>() {
-        let a = create_fake_shard::<F>(&[to_curve::<F>(1), to_curve::<F>(0)], &[1, 4, 7]);
-        let b = create_fake_shard::<F>(&[to_curve::<F>(0), to_curve::<F>(2)], &[2, 5, 8]);
-        let c = create_fake_shard::<F>(&[to_curve::<F>(3), to_curve::<F>(5)], &[3, 6, 9]);
+        let a = create_fake_shard::<F>(&[to_curve(1), to_curve(0)], &[1, 4, 7]);
+        let b = create_fake_shard::<F>(&[to_curve(0), to_curve(2)], &[2, 5, 8]);
+        let c = create_fake_shard::<F>(&[to_curve(3), to_curve(5)], &[3, 6, 9]);
 
-        assert!(combine::<F>(&[], &[]).is_none());
-        assert!(combine::<F>(
+        assert!(recode_with_coeffs::<F>(&[], &[]).is_none());
+        assert!(recode_with_coeffs(
             &[a.clone(), b.clone(), c.clone()],
-            &[to_curve::<F>(1), to_curve::<F>(2)]
+            &[to_curve(1), to_curve(2)]
         )
         .is_none());
         assert_eq!(
-            combine::<F>(
-                &[a, b, c],
-                &[to_curve::<F>(1), to_curve::<F>(2), to_curve::<F>(3)]
-            ),
-            Some(create_fake_shard::<F>(
-                &[to_curve::<F>(10), to_curve::<F>(19)],
+            recode_with_coeffs(&[a, b, c], &[to_curve(1), to_curve(2), to_curve(3)]),
+            Some(create_fake_shard(
+                &[to_curve(10), to_curve(19)],
                 &[14, 32, 50]
             ))
         );
diff --git a/src/field.rs b/src/field.rs
index 4bb055a9b54d415cf470081273d21fa1397cd805..cac315bced8d8222278307103d60955b78a33a99 100644
--- a/src/field.rs
+++ b/src/field.rs
@@ -95,8 +95,8 @@ mod tests {
     }
 
     fn split_and_merge_template<F: PrimeField>(bytes: &[u8], modulus: usize) {
-        let elements = field::split_data_into_field_elements::<F>(bytes, modulus);
-        let mut actual = merge_elements_into_bytes::<F>(&elements);
+        let elements: Vec<F> = field::split_data_into_field_elements(bytes, modulus);
+        let mut actual = merge_elements_into_bytes(&elements);
         actual.resize(bytes.len(), 0);
         assert_eq!(bytes, actual, "TEST | modulus: {modulus}");
     }
diff --git a/src/fs.rs b/src/fs.rs
index a3369663122957f051b6b60bdfa417c82ad2a57f..4d3fa506a92cdb0adf933263066da3fd7775f3a3 100644
--- a/src/fs.rs
+++ b/src/fs.rs
@@ -90,7 +90,7 @@ pub fn read_blocks<F: PrimeField, G: CurveGroup<ScalarField = F>>(
             let s = std::fs::read(filename)?;
             Ok((
                 f.clone(),
-                Block::<F, G>::deserialize_with_mode(&s[..], compress, validate)?,
+                Block::deserialize_with_mode(&s[..], compress, validate)?,
             ))
         })
         .collect()
diff --git a/src/lib.rs b/src/lib.rs
index 36da2376623cffacd97551bd1499fbc06230fb7f..541ffae700e6273321f2ecf8fc2d0dd7de7b345c 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -3,7 +3,8 @@ use ark_ec::CurveGroup;
 use ark_ff::PrimeField;
 use ark_poly::DenseUVPolynomial;
 use ark_serialize::{CanonicalDeserialize, CanonicalSerialize};
-use ark_std::{ops::Div, rand::RngCore};
+use ark_std::ops::Div;
+use ark_std::rand::RngCore;
 
 use tracing::{debug, info};
 
@@ -16,8 +17,7 @@ pub mod zk;
 
 use crate::{
     error::KomodoError,
-    fec::combine,
-    linalg::Matrix,
+    fec::Shard,
     zk::{Commitment, Powers},
 };
 
@@ -28,7 +28,7 @@ use crate::{
 #[derive(Debug, Default, Clone, PartialEq, CanonicalSerialize, CanonicalDeserialize)]
 pub struct Block<F: PrimeField, G: CurveGroup<ScalarField = F>> {
     pub shard: fec::Shard<F>,
-    commit: Vec<Commitment<F, G>>,
+    proof: Vec<Commitment<F, G>>,
 }
 
 impl<F: PrimeField, G: CurveGroup<ScalarField = F>> std::fmt::Display for Block<F, G> {
@@ -70,7 +70,7 @@ impl<F: PrimeField, G: CurveGroup<ScalarField = F>> std::fmt::Display for Block<
         write!(f, "}}")?;
         write!(f, ",")?;
         write!(f, "commits: [")?;
-        for commit in &self.commit {
+        for commit in &self.proof {
             write!(f, r#""{}","#, commit.0)?;
         }
         write!(f, "]")?;
@@ -80,16 +80,47 @@ impl<F: PrimeField, G: CurveGroup<ScalarField = F>> std::fmt::Display for Block<
     }
 }
 
-/// compute encoded and proven blocks of data from some data and an encoding
-/// method
+/// compute a recoded block from an arbitrary set of blocks
+///
+/// coefficients will be drawn at random, one for each block.
+///
+/// if the blocks appear to come from different data, e.g. if the commits are
+/// different, an error will be returned.
 ///
 /// > **Note**
-/// > this is a wrapper around [`fec::encode`].
-pub fn encode<F, G, P>(
+/// > this is a wrapper around [`fec::recode_random`].
+pub fn recode<F: PrimeField, G: CurveGroup<ScalarField = F>>(
+    blocks: &[Block<F, G>],
+    rng: &mut impl RngCore,
+) -> Result<Option<Block<F, G>>, KomodoError> {
+    for (i, (b1, b2)) in blocks.iter().zip(blocks.iter().skip(1)).enumerate() {
+        if b1.proof != b2.proof {
+            return Err(KomodoError::IncompatibleBlocks(format!(
+                "proofs are not the same at {}: {:?} vs {:?}",
+                i, b1.proof, b2.proof
+            )));
+        }
+    }
+    let shard = match fec::recode_random(
+        &blocks.iter().map(|b| b.shard.clone()).collect::<Vec<_>>(),
+        rng,
+    )? {
+        Some(s) => s,
+        None => return Ok(None),
+    };
+
+    Ok(Some(Block {
+        shard,
+        proof: blocks[0].proof.clone(),
+    }))
+}
+
+/// compute the Semi-AVID proof for some data
+pub fn prove<F, G, P>(
     bytes: &[u8],
-    encoding_mat: &Matrix<F>,
     powers: &Powers<F, G>,
-) -> Result<Vec<Block<F, G>>, KomodoError>
+    k: usize,
+) -> Result<Vec<Commitment<F, G>>, KomodoError>
 where
     F: PrimeField,
     G: CurveGroup<ScalarField = F>,
@@ -98,10 +129,8 @@ where
 {
     info!("encoding and proving {} bytes", bytes.len());
 
-    let k = encoding_mat.height;
-
     debug!("splitting bytes into polynomials");
-    let elements = field::split_data_into_field_elements::<F>(bytes, k);
+    let elements = field::split_data_into_field_elements(bytes, k);
     let polynomials = elements
         .chunks(k)
         .map(|c| P::from_coefficients_vec(c.to_vec()))
@@ -120,68 +149,25 @@ where
     debug!("committing the polynomials");
     let commits = zk::batch_commit(powers, &polynomials_to_commit)?;
 
-    Ok(fec::encode(bytes, encoding_mat)?
+    Ok(commits)
+}
+
+/// attach a Semi-AVID proof to a collection of encoded shards
+#[inline(always)]
+pub fn build<F, G, P>(shards: &[Shard<F>], proof: &[Commitment<F, G>]) -> Vec<Block<F, G>>
+where
+    F: PrimeField,
+    G: CurveGroup<ScalarField = F>,
+    P: DenseUVPolynomial<F>,
+    for<'a, 'b> &'a P: Div<&'b P, Output = P>,
+{
+    shards
         .iter()
         .map(|s| Block {
             shard: s.clone(),
-            commit: commits.clone(),
+            proof: proof.to_vec(),
         })
-        .collect::<Vec<_>>())
-}
-
-/// compute a recoded block from an arbitrary set of blocks
-///
-/// coefficients will be drawn at random, one for each block.
-///
-/// if the blocks appear to come from different data, e.g. if `k` is not the
-/// same or the hash of the data is different, an error will be returned.
-///
-/// > **Note**
-/// > this is a wrapper around [`fec::combine`].
-pub fn recode<F: PrimeField, G: CurveGroup<ScalarField = F>, R: RngCore>(
-    blocks: &[Block<F, G>],
-    rng: &mut R,
-) -> Result<Option<Block<F, G>>, KomodoError> {
-    let coeffs = blocks.iter().map(|_| F::rand(rng)).collect::<Vec<_>>();
-
-    for (i, (b1, b2)) in blocks.iter().zip(blocks.iter().skip(1)).enumerate() {
-        if b1.shard.k != b2.shard.k {
-            return Err(KomodoError::IncompatibleBlocks(format!(
-                "k is not the same at {}: {} vs {}",
-                i, b1.shard.k, b2.shard.k
-            )));
-        }
-        if b1.shard.hash != b2.shard.hash {
-            return Err(KomodoError::IncompatibleBlocks(format!(
-                "hash is not the same at {}: {:?} vs {:?}",
-                i, b1.shard.hash, b2.shard.hash
-            )));
-        }
-        if b1.shard.size != b2.shard.size {
-            return Err(KomodoError::IncompatibleBlocks(format!(
-                "size is not the same at {}: {} vs {}",
-                i, b1.shard.size, b2.shard.size
-            )));
-        }
-        if b1.commit != b2.commit {
-            return Err(KomodoError::IncompatibleBlocks(format!(
-                "commits are not the same at {}: {:?} vs {:?}",
-                i, b1.commit, b2.commit
-            )));
-        }
-    }
-    let shard = match combine(
-        &blocks.iter().map(|b| b.shard.clone()).collect::<Vec<_>>(),
-        &coeffs,
-    ) {
-        Some(s) => s,
-        None => return Ok(None),
-    };
-
-    Ok(Some(Block {
-        shard,
-        commit: blocks[0].commit.clone(),
-    }))
+        .collect::<Vec<_>>()
 }
 
 /// verify that a single block of encoded and proven data is valid
@@ -204,9 +190,9 @@ where
         .linear_combination
         .iter()
         .enumerate()
-        .map(|(i, w)| Into::<G>::into(block.commit[i].0) * w)
+        .map(|(i, w)| block.proof[i].0.into() * w)
         .sum();
-    Ok(Into::<G>::into(commit.0) == rhs)
+    Ok(commit.0.into() == rhs)
 }
 
 #[cfg(test)]
@@ -218,11 +204,11 @@ mod tests {
     use ark_std::{ops::Div, test_rng};
 
     use crate::{
-        encode,
+        build,
         error::KomodoError,
-        fec::{decode, Shard},
+        fec::{decode, encode, Shard},
         linalg::Matrix,
-        recode, verify,
+        prove, recode, verify,
         zk::{setup, Commitment},
     };
 
@@ -230,6 +216,12 @@ mod tests {
         include_bytes!("../tests/dragoon_133x133.png").to_vec()
     }
 
+    macro_rules! full {
+        ($b:ident, $p:ident, $m:ident) => {
+            build::<F, G, P>(&encode($b, $m)?, &prove($b, &$p, $m.height)?)
+        };
+    }
+
     fn verify_template<F, G, P>(bytes: &[u8], encoding_mat: &Matrix<F>) -> Result<(), KomodoError>
     where
         F: PrimeField,
@@ -239,11 +231,12 @@ mod tests {
     {
         let rng = &mut test_rng();
 
-        let powers = setup(bytes.len(), rng)?;
-        let blocks = encode::<F, G, P>(bytes, encoding_mat, &powers)?;
+        let powers = setup::<F, G>(bytes.len(), rng)?;
+
+        let blocks = full!(bytes, powers, encoding_mat);
 
         for block in &blocks {
-            assert!(verify::<F, G, P>(block, &powers)?);
+            assert!(verify(block, &powers)?);
         }
 
         Ok(())
@@ -262,20 +255,21 @@ mod tests {
         let rng = &mut test_rng();
 
         let powers = setup(bytes.len(), rng)?;
-        let blocks = encode::<F, G, P>(bytes, encoding_mat, &powers)?;
+
+        let blocks = full!(bytes, powers, encoding_mat);
 
         for block in &blocks {
-            assert!(verify::<F, G, P>(block, &powers)?);
+            assert!(verify(block, &powers)?);
         }
 
         let mut corrupted_block = blocks[0].clone();
         // modify a field in the struct b to corrupt the block proof without corrupting the data serialization
         let a = F::from_le_bytes_mod_order(&123u128.to_le_bytes());
-        let mut commits: Vec<G> = corrupted_block.commit.iter().map(|c| c.0.into()).collect();
+        let mut commits: Vec<G> = corrupted_block.proof.iter().map(|c| c.0.into()).collect();
         commits[0] = commits[0].mul(a.pow([4321_u64]));
-        corrupted_block.commit = commits.iter().map(|&c| Commitment(c.into())).collect();
+        corrupted_block.proof = commits.iter().map(|&c| Commitment(c.into())).collect();
 
-        assert!(!verify::<F, G, P>(&corrupted_block, &powers)?);
+        assert!(!verify(&corrupted_block, &powers)?);
 
         Ok(())
     }
@@ -292,14 +286,15 @@ mod tests {
     {
         let rng = &mut test_rng();
 
-        let powers = setup(bytes.len(), rng)?;
-        let blocks = encode::<F, G, P>(bytes, encoding_mat, &powers)?;
+        let powers = setup::<F, G>(bytes.len(), rng)?;
+
+        let blocks = full!(bytes, powers, encoding_mat);
 
-        assert!(verify::<F, G, P>(
+        assert!(verify(
             &recode(&blocks[2..=3], rng).unwrap().unwrap(),
             &powers
         )?);
-        assert!(verify::<F, G, P>(
+        assert!(verify(
             &recode(&[blocks[3].clone(), blocks[5].clone()], rng)
                 .unwrap()
                 .unwrap(),
@@ -321,13 +316,13 @@ mod tests {
     {
         let rng = &mut test_rng();
 
-        let powers = setup(bytes.len(), rng)?;
-        let blocks: Vec<Shard<F>> = encode::<F, G, P>(bytes, encoding_mat, &powers)?
-            .iter()
-            .map(|b| b.shard.clone())
-            .collect();
+        let powers = setup::<F, G>(bytes.len(), rng)?;
 
-        assert_eq!(bytes, decode::<F>(blocks).unwrap());
+        let blocks = full!(bytes, powers, encoding_mat);
+
+        let shards: Vec<Shard<F>> = blocks.iter().map(|b| b.shard.clone()).collect();
+
+        assert_eq!(bytes, decode(shards).unwrap());
 
         Ok(())
     }
@@ -344,8 +339,9 @@ mod tests {
     {
         let rng = &mut test_rng();
 
-        let powers = setup(bytes.len(), rng)?;
-        let blocks = encode::<F, G, P>(bytes, encoding_mat, &powers)?;
+        let powers = setup::<F, G>(bytes.len(), rng)?;
+
+        let blocks = full!(bytes, powers, encoding_mat);
 
         let b_0_1 = recode(&blocks[0..=1], rng).unwrap().unwrap();
         let shards = vec![
@@ -353,7 +349,7 @@ mod tests {
             blocks[2].shard.clone(),
             blocks[3].shard.clone(),
         ];
-        assert_eq!(bytes, decode::<F>(shards).unwrap());
+        assert_eq!(bytes, decode(shards).unwrap());
 
         let b_0_1 = recode(&[blocks[0].clone(), blocks[1].clone()], rng)
             .unwrap()
@@ -363,7 +359,7 @@ mod tests {
             blocks[1].shard.clone(),
             b_0_1.shard,
         ];
-        assert!(decode::<F>(shards).is_err());
+        assert!(decode(shards).is_err());
 
         let b_0_1 = recode(&blocks[0..=1], rng).unwrap().unwrap();
         let b_2_3 = recode(&blocks[2..=3], rng).unwrap().unwrap();
@@ -371,12 +367,12 @@ mod tests {
             .unwrap()
             .unwrap();
         let shards = vec![b_0_1.shard, b_2_3.shard, b_1_4.shard];
-        assert_eq!(bytes, decode::<F>(shards).unwrap());
+        assert_eq!(bytes, decode(shards).unwrap());
 
         let fully_recoded_shards = (0..3)
             .map(|_| recode(&blocks[0..=2], rng).unwrap().unwrap().shard)
             .collect();
-        assert_eq!(bytes, decode::<F>(fully_recoded_shards).unwrap());
+        assert_eq!(bytes, decode(fully_recoded_shards).unwrap());
 
         Ok(())
     }
diff --git a/src/main.rs b/src/main.rs
index 0d9083dcd1fdcf5b666164c54727b406bfb6d0a2..75a308bb0c22fc2c155632a1e0c2266fdd0d2ae5 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -13,12 +13,12 @@ use ark_std::rand::RngCore;
 use tracing::{info, warn};
 
 use komodo::{
-    encode,
+    build,
     error::KomodoError,
-    fec::{decode, Shard},
+    fec::{self, decode, Shard},
     fs,
     linalg::Matrix,
-    recode, verify,
+    prove, recode, verify,
     zk::{self, Powers},
     Block,
 };
@@ -118,21 +118,20 @@ fn throw_error(code: i32, message: &str) {
     exit(code);
 }
 
-fn generate_random_powers<F, G, P, R>(
+fn generate_random_powers<F, G, P>(
     n: usize,
     powers_dir: &Path,
     powers_filename: Option<&str>,
-    rng: &mut R,
+    rng: &mut impl RngCore,
 ) -> Result<()>
 where
     F: PrimeField,
     G: CurveGroup<ScalarField = F>,
     P: DenseUVPolynomial<F>,
     for<'a, 'b> &'a P: Div<&'b P, Output = P>,
-    R: RngCore,
 {
     info!("generating new powers");
-    let powers = zk::setup::<_, F, G>(zk::nb_elements_in_setup::<F>(n), rng)?;
+    let powers = zk::setup::<F, G>(zk::nb_elements_in_setup::<F>(n), rng)?;
 
     fs::dump(&powers, powers_dir, powers_filename, COMPRESS)?;
 
@@ -189,7 +188,7 @@ fn main() {
     let powers_file = powers_dir.join(powers_filename);
 
     if do_generate_powers {
-        generate_random_powers::<Fr, G1Projective, DensePolynomial<Fr>, _>(
+        generate_random_powers::<Fr, G1Projective, DensePolynomial<Fr>>(
             nb_bytes,
             &powers_dir,
             Some(powers_filename),
@@ -286,7 +285,7 @@ fn main() {
     } else {
         warn!("could not read powers from `{:?}`", powers_file);
         info!("regenerating temporary powers");
-        zk::setup::<_, Fr, G1Projective>(zk::nb_elements_in_setup::<Fr>(nb_bytes), &mut rng)
+        zk::setup::<Fr, G1Projective>(zk::nb_elements_in_setup::<Fr>(nb_bytes), &mut rng)
             .unwrap_or_else(|e| {
                 throw_error(1, &format!("could not generate powers: {}", e));
                 unreachable!()
@@ -324,16 +323,18 @@ fn main() {
         }
     };
 
-    let formatted_output = fs::dump_blocks(
-        &encode::<Fr, G1Projective, DensePolynomial<Fr>>(&bytes, &encoding_mat, &powers)
-            .unwrap_or_else(|e| {
-                throw_error(1, &format!("could not encode: {}", e));
-                unreachable!()
-            }),
-        &block_dir,
-        COMPRESS,
-    )
-    .unwrap_or_else(|e| {
+    let shards = fec::encode::<Fr>(&bytes, &encoding_mat).unwrap_or_else(|e| {
+        throw_error(1, &format!("could not encode: {}", e));
+        unreachable!()
+    });
+    let proof =
+        prove::<Fr, G1Projective, DensePolynomial<Fr>>(&bytes, &powers, k).unwrap_or_else(|e| {
+            throw_error(1, &format!("could not prove: {}", e));
+            unreachable!()
+        });
+    let blocks = build::<Fr, G1Projective, DensePolynomial<Fr>>(&shards, &proof);
+
+    let formatted_output = fs::dump_blocks(&blocks, &block_dir, COMPRESS).unwrap_or_else(|e| {
         throw_error(1, &format!("could not dump blocks: {}", e));
         unreachable!()
     });
diff --git a/src/zk.rs b/src/zk.rs
index acbbe9a7448caa19e6724c5dc400f91b88dd39e6..697963b4bd447551131bcffc87f6683fdee99a6d 100644
--- a/src/zk.rs
+++ b/src/zk.rs
@@ -35,9 +35,9 @@ impl<F: PrimeField, G: CurveGroup<ScalarField = F>> IntoIterator for Powers<F, G
 pub struct Commitment<F: PrimeField, G: CurveGroup<ScalarField = F>>(pub G::Affine);
 
 /// create a trusted setup of a given size, the expected maximum degree of the data
-pub fn setup<R: RngCore, F: PrimeField, G: CurveGroup<ScalarField = F>>(
+pub fn setup<F: PrimeField, G: CurveGroup<ScalarField = F>>(
     max_degree: usize,
-    rng: &mut R,
+    rng: &mut impl RngCore,
 ) -> Result<Powers<F, G>, KomodoError> {
     if max_degree < 1 {
         return Err(KomodoError::DegreeIsZero);
@@ -182,7 +182,7 @@ mod tests {
 
         let rng = &mut test_rng();
 
-        let powers = setup::<_, F, G>(degree, rng).unwrap();
+        let powers = setup::<F, G>(degree, rng).unwrap();
 
         assert_eq!(
             powers.len(),
@@ -201,7 +201,7 @@ mod tests {
     fn generate_invalid_setup_template<F: PrimeField, G: CurveGroup<ScalarField = F>>() {
         let rng = &mut test_rng();
 
-        let powers = setup::<_, F, G>(0, rng);
+        let powers = setup::<F, G>(0, rng);
         assert!(
             powers.is_err(),
             "creating a trusted setup for a degree 0 polynomial should NOT work"
@@ -212,7 +212,7 @@ mod tests {
             "message should say the degree is zero"
         );
         assert!(
-            setup::<_, F, G>(1, rng).is_ok(),
+            setup::<F, G>(1, rng).is_ok(),
             "creating a trusted setup for any polynomial with degree at least 1 should work"
         );
     }
@@ -232,7 +232,7 @@ mod tests {
 
         let rng = &mut test_rng();
 
-        let powers = setup::<_, F, G>(degree, rng).unwrap();
+        let powers = setup::<F, G>(degree, rng).unwrap();
 
         assert!(
             commit_to_test(&powers, &P::rand(degree - 1, rng)).is_ok(),