Explorar o código

zkas: Implement debug symbols in the compiled binary

x hai 7 meses
pai
achega
3aa0e3b722

+ 1 - 1
bin/darkirc/src/crypto/rln.rs

@@ -133,7 +133,7 @@ impl RlnIdentity {
             vec![epoch, external_nullifier, x, y, internal_nullifier, identity_root.inner()];
 
         info!(target: "crypto::rln::create_proof", "[RLN] Creating proof for event {}", event.id());
-        let signal_zkbin = ZkBinary::decode(RLN2_SIGNAL_ZKBIN)?;
+        let signal_zkbin = ZkBinary::decode(RLN2_SIGNAL_ZKBIN, false)?;
         let signal_circuit = ZkCircuit::new(witnesses, &signal_zkbin);
 
         let proof = Proof::create(proving_key, &[signal_circuit], &public_inputs, &mut OsRng)?;

+ 1 - 1
bin/darkirc/src/irc/client.rs

@@ -603,7 +603,7 @@ impl Client {
         let identity_tree: MerkleTree = deserialize_async(&identity_tree).await?;
 
         // Retrieve the ZK proving key from the db
-        let signal_zkbin = ZkBinary::decode(RLN2_SIGNAL_ZKBIN)?;
+        let signal_zkbin = ZkBinary::decode(RLN2_SIGNAL_ZKBIN, false)?;
         let signal_circuit = ZkCircuit::new(empty_witnesses(&signal_zkbin)?, &signal_zkbin);
         let Some(proving_key) = self.server.server_store.get("rlnv2-diff-signal-pk")? else {
             return Err(Error::DatabaseError(

+ 3 - 3
bin/darkirc/src/irc/server.rs

@@ -155,7 +155,7 @@ impl IrcServer {
         let rln_identity_store = darkirc.sled.open_tree("rln_identity_store")?;
 
         // Generate RLN proving and verifying keys, if needed
-        let rln_signal_zkbin = ZkBinary::decode(RLN2_SIGNAL_ZKBIN)?;
+        let rln_signal_zkbin = ZkBinary::decode(RLN2_SIGNAL_ZKBIN, false)?;
         let rln_signal_circuit =
             ZkCircuit::new(empty_witnesses(&rln_signal_zkbin)?, &rln_signal_zkbin);
 
@@ -184,7 +184,7 @@ impl IrcServer {
 
         if server_store.get("rlnv2-diff-slash-pk")?.is_none() {
             info!(target: "irc::server", "[RLN] Creating RlnV2_Diff_Slash ProvingKey");
-            let zkbin = ZkBinary::decode(RLN2_SLASH_ZKBIN)?;
+            let zkbin = ZkBinary::decode(RLN2_SLASH_ZKBIN, false)?;
             let circuit = ZkCircuit::new(empty_witnesses(&zkbin).unwrap(), &zkbin);
             let provingkey = ProvingKey::build(zkbin.k, &circuit);
             let mut buf = vec![];
@@ -194,7 +194,7 @@ impl IrcServer {
 
         if server_store.get("rlnv2-diff-slash-vk")?.is_none() {
             info!(target: "irc::server", "[RLN] Creating RlnV2_Diff_Slash VerifyingKey");
-            let zkbin = ZkBinary::decode(RLN2_SIGNAL_ZKBIN)?;
+            let zkbin = ZkBinary::decode(RLN2_SIGNAL_ZKBIN, false)?;
             let circuit = ZkCircuit::new(empty_witnesses(&zkbin).unwrap(), &zkbin);
             let verifyingkey = VerifyingKey::build(zkbin.k, &circuit);
             let mut buf = vec![];

+ 19 - 19
bin/drk/src/dao.rs

@@ -2126,7 +2126,7 @@ impl Drk {
             return Err(Error::Custom("Fee circuit not found".to_string()))
         };
 
-        let fee_zkbin = ZkBinary::decode(&fee_zkbin.1)?;
+        let fee_zkbin = ZkBinary::decode(&fee_zkbin.1, false)?;
 
         let fee_circuit = ZkCircuit::new(empty_witnesses(&fee_zkbin)?, &fee_zkbin);
 
@@ -2141,7 +2141,7 @@ impl Drk {
             return Err(Error::DatabaseError("[dao_mint] DAO Mint circuit not found".to_string()))
         };
 
-        let dao_mint_zkbin = ZkBinary::decode(&dao_mint_zkbin.1)?;
+        let dao_mint_zkbin = ZkBinary::decode(&dao_mint_zkbin.1, false)?;
 
         let dao_mint_circuit = ZkCircuit::new(empty_witnesses(&dao_mint_zkbin)?, &dao_mint_zkbin);
 
@@ -2464,7 +2464,7 @@ impl Drk {
             ))
         };
 
-        let fee_zkbin = ZkBinary::decode(&fee_zkbin.1)?;
+        let fee_zkbin = ZkBinary::decode(&fee_zkbin.1, false)?;
 
         let fee_circuit = ZkCircuit::new(empty_witnesses(&fee_zkbin)?, &fee_zkbin);
 
@@ -2490,8 +2490,8 @@ impl Drk {
             ))
         };
 
-        let propose_burn_zkbin = ZkBinary::decode(&propose_burn_zkbin.1)?;
-        let propose_main_zkbin = ZkBinary::decode(&propose_main_zkbin.1)?;
+        let propose_burn_zkbin = ZkBinary::decode(&propose_burn_zkbin.1, false)?;
+        let propose_main_zkbin = ZkBinary::decode(&propose_main_zkbin.1, false)?;
 
         let propose_burn_circuit =
             ZkCircuit::new(empty_witnesses(&propose_burn_zkbin)?, &propose_burn_zkbin);
@@ -2644,7 +2644,7 @@ impl Drk {
             return Err(Error::Custom("[dao_generic_proposal_tx] Fee circuit not found".to_string()))
         };
 
-        let fee_zkbin = ZkBinary::decode(&fee_zkbin.1)?;
+        let fee_zkbin = ZkBinary::decode(&fee_zkbin.1, false)?;
 
         let fee_circuit = ZkCircuit::new(empty_witnesses(&fee_zkbin)?, &fee_zkbin);
 
@@ -2670,8 +2670,8 @@ impl Drk {
             ))
         };
 
-        let propose_burn_zkbin = ZkBinary::decode(&propose_burn_zkbin.1)?;
-        let propose_main_zkbin = ZkBinary::decode(&propose_main_zkbin.1)?;
+        let propose_burn_zkbin = ZkBinary::decode(&propose_burn_zkbin.1, false)?;
+        let propose_main_zkbin = ZkBinary::decode(&propose_main_zkbin.1, false)?;
 
         let propose_burn_circuit =
             ZkCircuit::new(empty_witnesses(&propose_burn_zkbin)?, &propose_burn_zkbin);
@@ -2848,7 +2848,7 @@ impl Drk {
             return Err(Error::Custom("[dao_vote] Fee circuit not found".to_string()))
         };
 
-        let fee_zkbin = ZkBinary::decode(&fee_zkbin.1)?;
+        let fee_zkbin = ZkBinary::decode(&fee_zkbin.1, false)?;
 
         let fee_circuit = ZkCircuit::new(empty_witnesses(&fee_zkbin)?, &fee_zkbin);
 
@@ -2870,8 +2870,8 @@ impl Drk {
             return Err(Error::Custom("[dao_vote] DAO Vote Main circuit not found".to_string()))
         };
 
-        let dao_vote_burn_zkbin = ZkBinary::decode(&dao_vote_burn_zkbin.1)?;
-        let dao_vote_main_zkbin = ZkBinary::decode(&dao_vote_main_zkbin.1)?;
+        let dao_vote_burn_zkbin = ZkBinary::decode(&dao_vote_burn_zkbin.1, false)?;
+        let dao_vote_main_zkbin = ZkBinary::decode(&dao_vote_main_zkbin.1, false)?;
 
         let dao_vote_burn_circuit =
             ZkCircuit::new(empty_witnesses(&dao_vote_burn_zkbin)?, &dao_vote_burn_zkbin);
@@ -3110,9 +3110,9 @@ impl Drk {
             return Err(Error::Custom("[dao_exec_transfer] Fee circuit not found".to_string()))
         };
 
-        let mint_zkbin = ZkBinary::decode(&mint_zkbin.1)?;
-        let burn_zkbin = ZkBinary::decode(&burn_zkbin.1)?;
-        let fee_zkbin = ZkBinary::decode(&fee_zkbin.1)?;
+        let mint_zkbin = ZkBinary::decode(&mint_zkbin.1, false)?;
+        let burn_zkbin = ZkBinary::decode(&burn_zkbin.1, false)?;
+        let fee_zkbin = ZkBinary::decode(&fee_zkbin.1, false)?;
 
         let mint_circuit = ZkCircuit::new(empty_witnesses(&mint_zkbin)?, &mint_zkbin);
         let burn_circuit = ZkCircuit::new(empty_witnesses(&burn_zkbin)?, &burn_zkbin);
@@ -3155,10 +3155,10 @@ impl Drk {
             ))
         };
 
-        let dao_exec_zkbin = ZkBinary::decode(&dao_exec_zkbin.1)?;
-        let dao_auth_transfer_zkbin = ZkBinary::decode(&dao_auth_transfer_zkbin.1)?;
+        let dao_exec_zkbin = ZkBinary::decode(&dao_exec_zkbin.1, false)?;
+        let dao_auth_transfer_zkbin = ZkBinary::decode(&dao_auth_transfer_zkbin.1, false)?;
         let dao_auth_transfer_enc_coin_zkbin =
-            ZkBinary::decode(&dao_auth_transfer_enc_coin_zkbin.1)?;
+            ZkBinary::decode(&dao_auth_transfer_enc_coin_zkbin.1, false)?;
 
         let dao_exec_circuit = ZkCircuit::new(empty_witnesses(&dao_exec_zkbin)?, &dao_exec_zkbin);
         let dao_auth_transfer_circuit =
@@ -3403,7 +3403,7 @@ impl Drk {
         else {
             return Err(Error::Custom("[dao_exec_generic] Fee circuit not found".to_string()))
         };
-        let fee_zkbin = ZkBinary::decode(&fee_zkbin.1)?;
+        let fee_zkbin = ZkBinary::decode(&fee_zkbin.1, false)?;
         let fee_circuit = ZkCircuit::new(empty_witnesses(&fee_zkbin)?, &fee_zkbin);
         let fee_pk = ProvingKey::build(fee_zkbin.k, &fee_circuit);
 
@@ -3422,7 +3422,7 @@ impl Drk {
                 "[dao_exec_generic] DAO {namespace} circuit not found"
             )))
         };
-        let dao_exec_zkbin = ZkBinary::decode(&dao_exec_zkbin.1)?;
+        let dao_exec_zkbin = ZkBinary::decode(&dao_exec_zkbin.1, false)?;
         let dao_exec_circuit = ZkCircuit::new(empty_witnesses(&dao_exec_zkbin)?, &dao_exec_zkbin);
         let dao_exec_pk = ProvingKey::build(dao_exec_zkbin.k, &dao_exec_circuit);
 

+ 2 - 2
bin/drk/src/deploy.rs

@@ -540,7 +540,7 @@ impl Drk {
             return Err(Error::Custom("[deploy_contract] Fee circuit not found".to_string()))
         };
 
-        let fee_zkbin = ZkBinary::decode(&fee_zkbin.1)?;
+        let fee_zkbin = ZkBinary::decode(&fee_zkbin.1, false)?;
 
         let fee_circuit = ZkCircuit::new(empty_witnesses(&fee_zkbin)?, &fee_zkbin);
 
@@ -603,7 +603,7 @@ impl Drk {
             return Err(Error::Custom("[lock_contract] Fee circuit not found".to_string()))
         };
 
-        let fee_zkbin = ZkBinary::decode(&fee_zkbin.1)?;
+        let fee_zkbin = ZkBinary::decode(&fee_zkbin.1, false)?;
 
         let fee_circuit = ZkCircuit::new(empty_witnesses(&fee_zkbin)?, &fee_zkbin);
 

+ 1 - 1
bin/drk/src/money.rs

@@ -1400,7 +1400,7 @@ impl Drk {
             return Err(Error::Custom("Fee circuit not found".to_string()))
         };
 
-        let fee_zkbin = ZkBinary::decode(&fee_zkbin.1)?;
+        let fee_zkbin = ZkBinary::decode(&fee_zkbin.1, false)?;
 
         let fee_circuit = ZkCircuit::new(empty_witnesses(&fee_zkbin)?, &fee_zkbin);
 

+ 4 - 4
bin/drk/src/swap.rs

@@ -125,8 +125,8 @@ impl Drk {
             return Err(Error::Custom("Burn circuit not found".to_string()))
         };
 
-        let mint_zkbin = ZkBinary::decode(&mint_zkbin.1)?;
-        let burn_zkbin = ZkBinary::decode(&burn_zkbin.1)?;
+        let mint_zkbin = ZkBinary::decode(&mint_zkbin.1, false)?;
+        let burn_zkbin = ZkBinary::decode(&burn_zkbin.1, false)?;
 
         let mint_circuit = ZkCircuit::new(empty_witnesses(&mint_zkbin)?, &mint_zkbin);
         let burn_circuit = ZkCircuit::new(empty_witnesses(&burn_zkbin)?, &burn_zkbin);
@@ -228,8 +228,8 @@ impl Drk {
             return Err(Error::Custom("Burn circuit not found".to_string()))
         };
 
-        let mint_zkbin = ZkBinary::decode(&mint_zkbin.1)?;
-        let burn_zkbin = ZkBinary::decode(&burn_zkbin.1)?;
+        let mint_zkbin = ZkBinary::decode(&mint_zkbin.1, false)?;
+        let burn_zkbin = ZkBinary::decode(&burn_zkbin.1, false)?;
 
         let mint_circuit = ZkCircuit::new(empty_witnesses(&mint_zkbin)?, &mint_zkbin);
         let burn_circuit = ZkCircuit::new(empty_witnesses(&burn_zkbin)?, &burn_zkbin);

+ 5 - 5
bin/drk/src/token.rs

@@ -301,9 +301,9 @@ impl Drk {
             return Err(Error::Custom("Fee circuit not found".to_string()))
         };
 
-        let mint_zkbin = ZkBinary::decode(&mint_zkbin.1)?;
-        let auth_mint_zkbin = ZkBinary::decode(&auth_mint_zkbin.1)?;
-        let fee_zkbin = ZkBinary::decode(&fee_zkbin.1)?;
+        let mint_zkbin = ZkBinary::decode(&mint_zkbin.1, false)?;
+        let auth_mint_zkbin = ZkBinary::decode(&auth_mint_zkbin.1, false)?;
+        let fee_zkbin = ZkBinary::decode(&fee_zkbin.1, false)?;
 
         let mint_circuit = ZkCircuit::new(empty_witnesses(&mint_zkbin)?, &mint_zkbin);
         let auth_mint_circuit =
@@ -409,8 +409,8 @@ impl Drk {
             return Err(Error::Custom("Fee circuit not found".to_string()))
         };
 
-        let auth_mint_zkbin = ZkBinary::decode(&auth_mint_zkbin.1)?;
-        let fee_zkbin = ZkBinary::decode(&fee_zkbin.1)?;
+        let auth_mint_zkbin = ZkBinary::decode(&auth_mint_zkbin.1, false)?;
+        let fee_zkbin = ZkBinary::decode(&fee_zkbin.1, false)?;
 
         let auth_mint_circuit =
             ZkCircuit::new(empty_witnesses(&auth_mint_zkbin)?, &auth_mint_zkbin);

+ 3 - 3
bin/drk/src/transfer.rs

@@ -95,9 +95,9 @@ impl Drk {
             return Err(Error::Custom("Fee circuit not found".to_string()))
         };
 
-        let mint_zkbin = ZkBinary::decode(&mint_zkbin.1)?;
-        let burn_zkbin = ZkBinary::decode(&burn_zkbin.1)?;
-        let fee_zkbin = ZkBinary::decode(&fee_zkbin.1)?;
+        let mint_zkbin = ZkBinary::decode(&mint_zkbin.1, false)?;
+        let burn_zkbin = ZkBinary::decode(&burn_zkbin.1, false)?;
+        let fee_zkbin = ZkBinary::decode(&fee_zkbin.1, false)?;
 
         let mint_circuit = ZkCircuit::new(empty_witnesses(&mint_zkbin)?, &mint_zkbin);
         let burn_circuit = ZkCircuit::new(empty_witnesses(&burn_zkbin)?, &burn_zkbin);

+ 1 - 1
bin/zkas/src/main.rs

@@ -164,7 +164,7 @@ fn main() -> ExitCode {
     println!("Wrote output to {}", &output);
 
     if eflag {
-        let zkbin = ZkBinary::decode(&bincode).unwrap();
+        let zkbin = ZkBinary::decode(&bincode, true).unwrap();
         println!("{zkbin:#?}");
     }
 

+ 12 - 11
bin/zkrunner/zkrunner.py

@@ -28,17 +28,18 @@ from darkfi_sdk.zkas import (MockProver, ZkBinary, ZkCircuit, ProvingKey,
 def eprint(fstr, *args):
     print("error: " + fstr, *args, file=sys.stderr)
 
-def show_trace(opcodes, trace):
-    print(f"{'Line':<4} {'Opcode':<22} {'Type':<10} {'Values'}")
+def show_trace(zkbin, opcodes, trace):
+    print(f"{'Line':<6} {'Source':<12} {'Opcode':<22} {'Result':<20} {'Values'}")
     for i, (opcode, (optype, args)) in enumerate(zip(opcodes, trace)):
-        if args:
-            args = ", ".join([str(arg) for arg in args])
-            args = f"[{args}]"
-        else:
-            args = ""
-        opcode = str(opcode)
-        optype = str(optype)
-        print(f"{i:<4} {opcode:<22} {optype:<10} {args}")
+        # Get source location from debug info
+        loc = zkbin.opcode_location(i)
+        source = f"L{loc[0]}:C{loc[1]}" if loc else "-"
+
+        # Get result variable name for assignments
+        result = zkbin.heap_name(i) or "-"  # Simplified - would need proper heap tracking
+
+        args_str = f"[{', '.join(str(a) for a in args)}]" if args else ""
+        print(f"{i:<6} {source:<12} {str(opcode):<22} {result:<20} {args_str}")
 
 def load_circuit_witness(circuit, witness_file):
     # We attempt to decode the witnesses from the JSON file.
@@ -147,7 +148,7 @@ def main(witness_file, source_file, mock=False, trace=False):
             return -3
 
         if trace:
-            show_trace(zkbin.opcodes(), circuit.opvalues())
+            show_trace(zkbin, zkbin.opcodes(), circuit.opvalues())
 
         print("Verifying ZK proof...")
         verify_status = proof.verify(verifying_key, instances)

+ 40 - 5
doc/src/zkas/bincode.md

@@ -25,8 +25,8 @@ CONSTANT_TYPE CONSTANT_NAME
 CONSTANT_TYPE CONSTANT_NAME 
 ...
 .literal
-LITERAL
-LITERAL
+LITERAL_TYPE LITERAL_VALUE
+LITERAL_TYPE LITERAL_VALUE
 ...
 .witness
 WITNESS_TYPE
@@ -36,8 +36,10 @@ WITNESS_TYPE
 OPCODE ARG_NUM HEAP_TYPE HEAP_INDEX ... HEAP_TYPE HEAP_INDEX
 OPCODE ARG_NUM HEAP_TYPE HEAP_INDEX ... HEAP_TYPE HEAP_INDEX
 ...
-.debug
-TBD
+.debug (optional)
+NUM_OPCODES [LINE COLUMN] ...
+HEAP_SIZE [HEAP_NAME] ...
+NUM_LITERALS [LITERAL_NAME] ...
 ```
 
 Integers in the binary are encoded using variable-integer encoding.
@@ -129,7 +131,40 @@ the heap and become available for later references.
 
 ### `.debug`
 
-TBD
+The `.debug` section is optional and contains debug information that
+maps the compiled binary back to the original source code. This is
+useful for debugging circuit failures when only the compiled binary
+is available.
+
+> `NUM_OPCODES [LINE COLUMN] ... HEAP_SIZE [HEAP_NAME] ... NUM_LITERALS [LITERAL_NAME] ...`
+
+where:
+
+|    Element      |                            Description                           |
+|-----------------|------------------------------------------------------------------|
+| `NUM_OPCODES`   | Number of opcodes in the `.circuit` section (VarInt)             |
+| `LINE`          | Source line number for this opcode (VarInt)                      |
+| `COLUMN`        | Source column number for this opcode (VarInt)                    |
+| `HEAP_SIZE`     | Total number of entries on the heap (VarInt)                     |
+| `HEAP_NAME`     | Variable name for this heap entry (String)                       |
+| `NUM_LITERALS`  | Number of literals in the `.literal` section (VarInt)            |
+| `LITERAL_NAME`  | The literal value as a string, e.g., "42" (String)               |
+
+The heap names are serialized in heap order: constants first, then
+witnesses, then assigned variables from circuit statements. This
+ordering matches the order in which items are pushed onto the heap
+during compilation.
+
+Using the debug info, a debugger or tracing tool can display output
+such as:
+
+```
+Line   Source       Opcode                 Variable             Value
+0      L23:C5       EcMulShort             token_commit         [0x3a2f..., 0x91bc...]
+1      L24:C5       EcMulBase              rcpt_commit          [0x7d1e..., 0x44fa...]
+2      L25:C5       EcAdd                  commitment           [0x8b3c..., 0x22de...]
+3      L26:C5       ConstrainInstance      -                    -
+```
 
 ## Syntax Reference
 

+ 1 - 1
script/research/gg/src/main.rs

@@ -267,7 +267,7 @@ fn main() -> Result<()> {
                         continue
                     }
                     let mut reader = Cursor::new(pk);
-                    let zkbin = ZkBinary::decode(&bincode)?;
+                    let zkbin = ZkBinary::decode(&bincode, false)?;
                     let circuit = ZkCircuit::new(empty_witnesses(&zkbin)?, &zkbin);
                     let proving_key = ProvingKey::read(&mut reader, circuit)?;
                     mint = Some((proving_key, zkbin));

+ 1 - 1
script/research/rlnv2/src/main.rs

@@ -97,7 +97,7 @@ fn main() {
     // Signalling
     // ==========
     let signal_zkbin = include_bytes!("../signal.zk.bin");
-    let signal_zkbin = ZkBinary::decode(signal_zkbin).unwrap();
+    let signal_zkbin = ZkBinary::decode(signal_zkbin, false).unwrap();
     let signal_empty_circuit =
         ZkCircuit::new(empty_witnesses(&signal_zkbin).unwrap(), &signal_zkbin);
 

+ 4 - 4
script/research/zkvm-metering/generator/src/main.rs

@@ -17,16 +17,16 @@
  */
 
 use std::{
-    fs::{File, read_dir},
+    fs::{read_dir, File},
     io::{Read, Write},
     path::Path,
 };
 
 use darkfi::{
-    zk::{Proof, ProvingKey, VerifyingKey, Witness, ZkCircuit, empty_witnesses},
+    zk::{empty_witnesses, Proof, ProvingKey, VerifyingKey, Witness, ZkCircuit},
     zkas::ZkBinary,
 };
-use darkfi_sdk::pasta::{Eq, Fp, pallas::Base};
+use darkfi_sdk::pasta::{pallas::Base, Eq, Fp};
 use darkfi_serial::serialize;
 use halo2_proofs::dev::CircuitCost;
 use rand::rngs::OsRng;
@@ -83,7 +83,7 @@ fn main() {
         let mut file = File::open(&path).unwrap();
         let mut buf = vec![];
         file.read_to_end(&mut buf).unwrap();
-        let zkbin = ZkBinary::decode(&buf).unwrap();
+        let zkbin = ZkBinary::decode(&buf, false).unwrap();
 
         // Get witnesses and public inputs for that particular zk file
         let (witnesses, public_inputs) = retrieve_proof_inputs(name);

+ 3 - 3
script/research/zkvm-metering/verifier/src/main.rs

@@ -24,9 +24,9 @@ use std::{
 };
 
 use darkfi::{
-    Result,
-    zk::{Proof, VerifyingKey, ZkCircuit, empty_witnesses},
+    zk::{empty_witnesses, Proof, VerifyingKey, ZkCircuit},
     zkas::ZkBinary,
+    Result,
 };
 use darkfi_sdk::pasta::pallas::Base;
 use darkfi_serial::deserialize;
@@ -66,7 +66,7 @@ fn main() -> Result<()> {
     file.read_to_end(&mut public_inputs_bin)?;
 
     // Deserialize and Verify
-    let zkbin = ZkBinary::decode(&bincode)?;
+    let zkbin = ZkBinary::decode(&bincode, false)?;
     let verifier_witnesses = empty_witnesses(&zkbin)?;
 
     // Create the circuit

+ 2 - 2
src/blockchain/contract_store.rs

@@ -188,7 +188,7 @@ impl ContractStore {
         let (zkbin, vkbin): (Vec<u8>, Vec<u8>) = deserialize(&zkas_bytes).unwrap();
 
         // The first vec is the compiled zkas binary
-        let zkbin = ZkBinary::decode(&zkbin).unwrap();
+        let zkbin = ZkBinary::decode(&zkbin, false).unwrap();
 
         // Construct the circuit to be able to read the VerifyingKey
         let circuit = ZkCircuit::new(empty_witnesses(&zkbin).unwrap(), &zkbin);
@@ -484,7 +484,7 @@ impl ContractStoreOverlay {
         let (zkbin, vkbin): (Vec<u8>, Vec<u8>) = deserialize(&zkas_bytes).unwrap();
 
         // The first vec is the compiled zkas binary
-        let zkbin = ZkBinary::decode(&zkbin).unwrap();
+        let zkbin = ZkBinary::decode(&zkbin, false).unwrap();
 
         // Construct the circuit to be able to read the VerifyingKey
         let circuit = ZkCircuit::new(empty_witnesses(&zkbin).unwrap(), &zkbin);

+ 1 - 1
src/contract/test-harness/src/lib.rs

@@ -266,7 +266,7 @@ impl TestHarness {
         let mut proving_keys = HashMap::new();
         for (bincode, namespace, pk) in pks {
             let mut reader = Cursor::new(pk);
-            let zkbin = ZkBinary::decode(&bincode)?;
+            let zkbin = ZkBinary::decode(&bincode, false)?;
             let circuit = ZkCircuit::new(empty_witnesses(&zkbin)?, &zkbin);
             let proving_key = ProvingKey::read(&mut reader, circuit)?;
             proving_keys.insert(namespace, (proving_key, zkbin));

+ 1 - 1
src/contract/test-harness/src/vks.rs

@@ -143,7 +143,7 @@ pub fn get_cached_pks_and_vks() -> Result<(Pks, Vks)> {
     let mut vks = vec![];
 
     for bincode in bins.iter() {
-        let zkbin = ZkBinary::decode(bincode)?;
+        let zkbin = ZkBinary::decode(bincode, false)?;
         debug!("Building PK for {}", zkbin.namespace);
         let witnesses = empty_witnesses(&zkbin)?;
         let circuit = ZkCircuit::new(witnesses, &zkbin);

+ 1 - 1
src/runtime/import/db.rs

@@ -890,7 +890,7 @@ pub(crate) fn zkas_db_set(mut ctx: FunctionEnvMut<Env>, ptr: WasmPtr<u8>, ptr_le
     };
 
     // Validate the bytes by decoding them into the ZkBinary format
-    let zkbin = match ZkBinary::decode(&zkbin_bytes) {
+    let zkbin = match ZkBinary::decode(&zkbin_bytes, false) {
         Ok(zkbin) => zkbin,
         Err(e) => {
             error!(

+ 15 - 2
src/sdk/python/src/zkas.rs

@@ -67,7 +67,7 @@ impl ZkBinary {
             analyzer.witnesses,
             analyzer.statements,
             analyzer.literals,
-            true,
+            false,
         );
 
         let bincode = compiler.compile().unwrap();
@@ -77,7 +77,7 @@ impl ZkBinary {
 
     #[staticmethod]
     fn decode(bytes: Vec<u8>) -> Self {
-        let bincode = decoder::ZkBinary::decode(bytes.as_slice()).unwrap();
+        let bincode = decoder::ZkBinary::decode(bytes.as_slice(), true).unwrap();
         Self(bincode)
     }
 
@@ -88,6 +88,18 @@ impl ZkBinary {
     fn opcodes(&self) -> Vec<ZkOpcode> {
         self.0.opcodes.iter().map(|op| ZkOpcode(op.0)).collect()
     }
+
+    fn opcode_location(&self, opcode_idx: usize) -> Option<(usize, usize)> {
+        self.0.opcode_location(opcode_idx)
+    }
+
+    fn heap_name(&self, heap_idx: usize) -> Option<&str> {
+        self.0.heap_name(heap_idx)
+    }
+
+    fn literal_name(&self, literal_idx: usize) -> Option<&str> {
+        self.0.literal_name(literal_idx)
+    }
 }
 
 #[pyclass(eq, eq_int)]
@@ -285,6 +297,7 @@ impl Proof {
             literals: Vec::new(),
             witnesses: Vec::new(),
             opcodes: Vec::new(),
+            debug_info: None,
         };
         let empty_circuit = zk::vm::ZkCircuit::new(Vec::new(), &zkbin);
         let empty_py_circuit = ZkCircuit(empty_circuit, Vec::new(), zkbin);

+ 47 - 5
src/zkas/compiler.rs

@@ -22,6 +22,9 @@ use darkfi_serial::{serialize, VarInt};
 
 use super::{
     ast::{Arg, Constant, Literal, Statement, StatementType, Witness},
+    constants::{
+        SECTION_CIRCUIT, SECTION_CONSTANT, SECTION_DEBUG, SECTION_LITERAL, SECTION_WITNESS,
+    },
     error::ErrorEmitter,
     types::HeapType,
 };
@@ -81,7 +84,7 @@ impl Compiler {
 
         // In the .constant section of the binary, we write the constant's type,
         // and the name so the VM can look it up from `src/crypto/constants/`.
-        bincode.extend_from_slice(b".constant");
+        bincode.extend_from_slice(SECTION_CONSTANT);
         for i in &self.constants {
             tmp_heap.push(i.name.as_str());
             bincode.push(i.typ as u8);
@@ -91,7 +94,7 @@ impl Compiler {
         // Currently, our literals are only Uint64 types, in the binary we'll
         // add them here in the .literal section. In the VM, they will be on
         // their own heap, used for reference by opcodes.
-        bincode.extend_from_slice(b".literal");
+        bincode.extend_from_slice(SECTION_LITERAL);
         for i in &self.literals {
             bincode.push(i.typ as u8);
             bincode.extend_from_slice(&serialize(&i.name));
@@ -99,13 +102,13 @@ impl Compiler {
 
         // In the .witness section, we write all our witness types, on the heap
         // they're in order of appearance.
-        bincode.extend_from_slice(b".witness");
+        bincode.extend_from_slice(SECTION_WITNESS);
         for i in &self.witnesses {
             tmp_heap.push(i.name.as_str());
             bincode.push(i.typ as u8);
         }
 
-        bincode.extend_from_slice(b".circuit");
+        bincode.extend_from_slice(SECTION_CIRCUIT);
         for i in &self.statements {
             match i.typ {
                 StatementType::Assign => tmp_heap.push(&i.lhs.as_ref().unwrap().name),
@@ -155,7 +158,46 @@ impl Compiler {
             return Ok(bincode)
         }
 
-        // TODO: Otherwise, we proceed appending debug info.
+        // Otherwise, we proceed appending debug info.
+        bincode.extend_from_slice(SECTION_DEBUG);
+
+        // Write source locations for each opcode.
+        // This allows mapping runtime errors back to source lines.
+        bincode.extend_from_slice(&serialize(&VarInt(self.statements.len() as u64)));
+        for stmt in &self.statements {
+            bincode.extend_from_slice(&serialize(&VarInt(stmt.line as u64)));
+            // For column, use the lhs variable's column if available
+            let column = stmt.lhs.as_ref().map(|v| v.column).unwrap_or(0);
+            bincode.extend_from_slice(&serialize(&VarInt(column as u64)));
+        }
+
+        // Write heap variable names.
+        // The heap contains constants, witnesses, assigned variables (in order).
+        // This allows showing meaningful names instead of heap indices.
+        let heap_size = self.constants.len() +
+            self.witnesses.len() +
+            self.statements.iter().filter(|s| s.typ == StatementType::Assign).count();
+        bincode.extend_from_slice(&serialize(&VarInt(heap_size as u64)));
+
+        for constant in &self.constants {
+            bincode.extend_from_slice(&serialize(&constant.name));
+        }
+
+        for witness in &self.witnesses {
+            bincode.extend_from_slice(&serialize(&witness.name));
+        }
+
+        for stmt in &self.statements {
+            if stmt.typ == StatementType::Assign {
+                bincode.extend_from_slice(&serialize(&stmt.lhs.as_ref().unwrap().name));
+            }
+        }
+
+        // Write literal names (the literal values as strings, e.g. "42")
+        bincode.extend_from_slice(&serialize(&VarInt(self.literals.len() as u64)));
+        for literal in &self.literals {
+            bincode.extend_from_slice(&serialize(&literal.name));
+        }
 
         Ok(bincode)
     }

+ 7 - 0
src/zkas/constants.rs

@@ -33,3 +33,10 @@ pub const ALLOWED_FIELDS: [&str; 1] = ["pallas"];
 
 /// Maximum recursion depth for nested function calls
 pub const MAX_RECURSION_DEPTH: usize = 16;
+
+// Section markers in the binary format
+pub(super) const SECTION_CONSTANT: &[u8] = b".constant";
+pub(super) const SECTION_LITERAL: &[u8] = b".literal";
+pub(super) const SECTION_WITNESS: &[u8] = b".witness";
+pub(super) const SECTION_CIRCUIT: &[u8] = b".circuit";
+pub(super) const SECTION_DEBUG: &[u8] = b".debug";

+ 102 - 14
src/zkas/decoder.rs

@@ -20,23 +20,19 @@ use darkfi_serial::{deserialize_partial, VarInt};
 
 use super::{
     compiler::MAGIC_BYTES,
-    constants::{MAX_K, MAX_NS_LEN, MIN_BIN_SIZE},
+    constants::{
+        MAX_K, MAX_NS_LEN, MIN_BIN_SIZE, SECTION_CIRCUIT, SECTION_CONSTANT, SECTION_DEBUG,
+        SECTION_LITERAL, SECTION_WITNESS,
+    },
     types::HeapType,
     LitType, Opcode, VarType,
 };
 use crate::{Error::ZkasDecoderError as ZkasErr, Result};
 
-// Section markers in the binary format
-const SECTION_CONSTANT: &[u8] = b".constant";
-const SECTION_LITERAL: &[u8] = b".literal";
-const SECTION_WITNESS: &[u8] = b".witness";
-const SECTION_CIRCUIT: &[u8] = b".circuit";
-const SECTION_DEBUG: &[u8] = b".debug";
-
 /// A ZkBinary decoded from compiled zkas code.
 /// This is used by the zkvm.
 ///
-/// The binary format consits of:
+/// The binary format consists of:
 /// - Header: magic bytes, version, k param, namespace
 /// - `.constant` section: constant types and names
 /// - `.literal` section: literal types and values
@@ -52,9 +48,22 @@ pub struct ZkBinary {
     pub literals: Vec<(LitType, String)>,
     pub witnesses: Vec<VarType>,
     pub opcodes: Vec<(Opcode, Vec<(HeapType, usize)>)>,
+    pub debug_info: Option<DebugInfo>,
 }
 // ANCHOR_END: zkbinary-struct
 
+/// Debug information decoded from the optional .debug section
+/// Contains source mappings to help debug circuit failures.
+#[derive(Clone, Debug, Default)]
+pub struct DebugInfo {
+    /// Source locations (line, col) for each opcode
+    pub opcode_locations: Vec<(usize, usize)>,
+    /// Variable names for each heap entry (constants, witnesses, assigned vars in order)
+    pub heap_names: Vec<String>,
+    /// Literal values as strings
+    pub literal_names: Vec<String>,
+}
+
 // https://stackoverflow.com/questions/35901547/how-can-i-find-a-subsequence-in-a-u8-slice
 fn find_subslice(haystack: &[u8], needle: &[u8]) -> Option<usize> {
     haystack.windows(needle.len()).position(|window| window == needle)
@@ -125,11 +134,20 @@ impl SectionOffsets {
     fn circuit_bytes<'a>(&self, bytes: &'a [u8]) -> &'a [u8] {
         &bytes[self.circuit + SECTION_CIRCUIT.len()..self.debug]
     }
+
+    /// Extract the bytes for the debug section if present
+    fn debug_bytes<'a>(&self, bytes: &'a [u8]) -> Option<&'a [u8]> {
+        if self.debug < bytes.len() {
+            Some(&bytes[self.debug + SECTION_DEBUG.len()..])
+        } else {
+            None
+        }
+    }
 }
 
 impl ZkBinary {
     /// Decode a ZkBinary from compiled bytes
-    pub fn decode(bytes: &[u8]) -> Result<Self> {
+    pub fn decode(bytes: &[u8], decode_debug_symbols: bool) -> Result<Self> {
         // Ensure that bytes is a certain minimum length. Otherwise the code
         // below will panic due to an index out of bounds error.
         if bytes.len() < MIN_BIN_SIZE {
@@ -168,9 +186,15 @@ impl ZkBinary {
         let witnesses = Self::parse_witnesses(offsets.witness_bytes(bytes))?;
         let opcodes = Self::parse_circuit(offsets.circuit_bytes(bytes))?;
 
-        // TODO: Debug info
+        let mut debug_info = None;
+        if decode_debug_symbols {
+            debug_info = match offsets.debug_bytes(bytes) {
+                Some(debug_bytes) => Some(Self::parse_debug(debug_bytes)?),
+                None => None,
+            };
+        }
 
-        Ok(Self { namespace, k, constants, literals, witnesses, opcodes })
+        Ok(Self { namespace, k, constants, literals, witnesses, opcodes, debug_info })
     }
 
     fn parse_constants(bytes: &[u8]) -> Result<Vec<(VarType, String)>> {
@@ -280,6 +304,70 @@ impl ZkBinary {
 
         Ok(opcodes)
     }
+
+    fn parse_debug(bytes: &[u8]) -> Result<DebugInfo> {
+        let mut offset = 0;
+
+        // Parse opcode source locations
+        let (num_opcodes, len) = deserialize_partial::<VarInt>(&bytes[offset..])?;
+        offset += len;
+
+        let mut opcode_locations = Vec::with_capacity(num_opcodes.0 as usize);
+        for _ in 0..num_opcodes.0 {
+            let (line, len) = deserialize_partial::<VarInt>(&bytes[offset..])?;
+            offset += len;
+            let (column, len) = deserialize_partial::<VarInt>(&bytes[offset..])?;
+            offset += len;
+            opcode_locations.push((line.0 as usize, column.0 as usize));
+        }
+
+        // Parse heap var names
+        let (heap_size, len) = deserialize_partial::<VarInt>(&bytes[offset..])?;
+        offset += len;
+
+        let mut heap_names = Vec::with_capacity(heap_size.0 as usize);
+        for _ in 0..heap_size.0 {
+            let (name, len) = deserialize_partial::<String>(&bytes[offset..])?;
+            offset += len;
+            heap_names.push(name);
+        }
+
+        // Parse literal names
+        let (num_literals, len) = deserialize_partial::<VarInt>(&bytes[offset..])?;
+        offset += len;
+
+        let mut literal_names = Vec::with_capacity(num_literals.0 as usize);
+        for _ in 0..num_literals.0 {
+            let (name, len) = deserialize_partial::<String>(&bytes[offset..])?;
+            offset += len;
+            literal_names.push(name);
+        }
+
+        Ok(DebugInfo { opcode_locations, heap_names, literal_names })
+    }
+
+    /// Get the source location (line, column) for a given opcode index.
+    /// Returns `None` if debug info is not present or index is OOB.
+    pub fn opcode_location(&self, opcode_idx: usize) -> Option<(usize, usize)> {
+        self.debug_info.as_ref()?.opcode_locations.get(opcode_idx).copied()
+    }
+
+    /// Get the variable name for a given heap index.
+    /// Returns `None` if debug info is not present or index is OOB.
+    pub fn heap_name(&self, heap_idx: usize) -> Option<&str> {
+        self.debug_info.as_ref()?.heap_names.get(heap_idx).map(|s| s.as_str())
+    }
+
+    /// Get the literal name/value for a given literal index.
+    /// Returns `None` if debug info is not present or index is OOB.
+    pub fn literal_name(&self, literal_idx: usize) -> Option<&str> {
+        self.debug_info.as_ref()?.literal_names.get(literal_idx).map(|s| s.as_str())
+    }
+
+    /// Check if debug info is present
+    pub fn has_debug_info(&self) -> bool {
+        self.debug_info.is_some()
+    }
 }
 
 #[cfg(test)]
@@ -291,7 +379,7 @@ mod tests {
         // Out-of-memory panic from string deserialization.
         // Read `doc/src/zkas/bincode.md` to understand the input.
         let data = vec![11u8, 1, 177, 53, 1, 0, 0, 0, 0, 255, 0, 204, 200, 72, 72, 72, 72, 1];
-        let _dec = ZkBinary::decode(&data);
+        let _dec = ZkBinary::decode(&data, true);
     }
 
     #[test]
@@ -307,6 +395,6 @@ mod tests {
             116, 4, 2, 0, 2, 0, 0, 2, 2, 0, 3, 0, 1, 8, 2, 0, 4, 0, 5, 8, 1, 0, 6, 9, 1, 0, 6, 240,
             1, 0, 7, 240, 41, 0, 0, 0, 1, 0, 8,
         ];
-        let _dec = ZkBinary::decode(&data);
+        let _dec = ZkBinary::decode(&data, true);
     }
 }

+ 1 - 1
tests/halo2_vk_ser.rs

@@ -46,7 +46,7 @@ use darkfi::{
 #[test]
 fn halo2_vk_ser() -> Result<()> {
     let bincode = include_bytes!("../proof/opcodes.zk.bin");
-    let zkbin = ZkBinary::decode(bincode)?;
+    let zkbin = ZkBinary::decode(bincode, false)?;
 
     let verifier_witnesses = empty_witnesses(&zkbin)?;
 

+ 1 - 1
tests/smt.rs

@@ -34,7 +34,7 @@ use darkfi::{
 #[test]
 fn zkvm_smt() -> Result<()> {
     let bincode = include_bytes!("../proof/smt.zk.bin");
-    let zkbin = ZkBinary::decode(bincode)?;
+    let zkbin = ZkBinary::decode(bincode, false)?;
 
     let hasher = PoseidonFp::new();
     let store = MemoryStorageFp::new();

+ 1 - 1
tests/zkvm_opcodes.rs

@@ -46,7 +46,7 @@ use darkfi::{
 #[test]
 fn zkvm_opcodes() -> Result<()> {
     let bincode = include_bytes!("../proof/opcodes.zk.bin");
-    let zkbin = ZkBinary::decode(bincode)?;
+    let zkbin = ZkBinary::decode(bincode, false)?;
 
     // Values for the proof
     let value = 666_u64;