Repository: ergoplatform/sigma-rust (language: rust)

Issue:
[100 SigmaUSD] Create PoPowHeader type
In ergo-nipopow crate. Implement binary (ScorexSerializable trait) and JSON serialization. Use `Header` type from ergotree-ir (or from ergo-chain-types if #488 is implemented).
Scala reference implementation - https://github.com/ergoplatform/ergo/blob/95139ec990fc91f8f4c70a61468edf8834143329/src/main/scala/org/ergoplatform/modifiers/history/popow/PoPowHeader.scala#L13-L83


Discussion / hints:
I'm going to work on this one 👍 
~Hi @ross-weir any updates on this?~
We spoke on discord, I will take over this issue.


Fix patch:
diff --git a/bindings/ergo-lib-c-core/src/nipopow.rs b/bindings/ergo-lib-c-core/src/nipopow.rs
index 6eadc5397..ce5f00ab3 100644
--- a/bindings/ergo-lib-c-core/src/nipopow.rs
+++ b/bindings/ergo-lib-c-core/src/nipopow.rs
@@ -1,7 +1,7 @@
 //! Bindings for NiPoPow
 
 use crate::{
-    block_header::{BlockHeader, ConstBlockIdPtr},
+    block_header::{BlockHeader, BlockHeaderPtr, BlockId, ConstBlockIdPtr},
     collections::{Collection, CollectionPtr},
     util::{const_ptr_as_ref, mut_ptr_as_mut},
     Error,
@@ -13,6 +13,11 @@ pub struct NipopowProof(ergo_lib::ergo_nipopow::NipopowProof);
 pub type NipopowProofPtr = *mut NipopowProof;
 pub type ConstNipopowProofPtr = *const NipopowProof;
 
+#[derive(Debug, PartialEq, Eq)]
+pub struct PoPowHeader(ergo_lib::ergo_nipopow::PoPowHeader);
+pub type PoPowHeaderPtr = *mut PoPowHeader;
+pub type ConstPoPowHeaderPtr = *const PoPowHeader;
+
 /// Implementation of the ≥ algorithm from [`KMZ17`], see Algorithm 4
 ///
 /// [`KMZ17`]: https://fc20.ifca.ai/preproceedings/74.pdf
@@ -93,3 +98,47 @@ pub unsafe fn nipopow_verifier_process(
     verifier.0.process(new_proof.0.clone())?;
     Ok(())
 }
+
+pub unsafe fn popow_header_from_json(
+    json: &str,
+    popow_header_out: *mut PoPowHeaderPtr,
+) -> Result<(), Error> {
+    let popow_header_out = mut_ptr_as_mut(popow_header_out, "popow_header_out")?;
+    let proof = serde_json::from_str(json).map(PoPowHeader)?;
+    *popow_header_out = Box::into_raw(Box::new(proof));
+    Ok(())
+}
+
+pub unsafe fn popow_header_to_json(popow_header_ptr: ConstPoPowHeaderPtr) -> Result<String, Error> {
+    let proof = const_ptr_as_ref(popow_header_ptr, "popow_header_ptr")?;
+    let s = serde_json::to_string(&proof.0)?;
+    Ok(s)
+}
+
+pub unsafe fn popow_header_get_interlinks(
+    popow_header_ptr: ConstPoPowHeaderPtr,
+    interlinks_out: *mut CollectionPtr<BlockId>,
+) -> Result<(), Error> {
+    let popow_header = const_ptr_as_ref(popow_header_ptr, "popow_header_ptr")?;
+    let interlinks_out = mut_ptr_as_mut(interlinks_out, "interlinks_out")?;
+    *interlinks_out = Box::into_raw(Box::new(Collection(
+        popow_header
+            .0
+            .interlinks
+            .iter()
+            .cloned()
+            .map(BlockId)
+            .collect(),
+    )));
+    Ok(())
+}
+
+pub unsafe fn popow_header_get_header(
+    popow_header_ptr: ConstPoPowHeaderPtr,
+    header_out: *mut BlockHeaderPtr,
+) -> Result<(), Error> {
+    let popow_header_ptr = const_ptr_as_ref(popow_header_ptr, "popow_header_ptr")?;
+    let header_out = mut_ptr_as_mut(header_out, "header_out")?;
+    *header_out = Box::into_raw(Box::new(BlockHeader(popow_header_ptr.0.header.clone())));
+    Ok(())
+}
diff --git a/bindings/ergo-lib-c/src/block_header.rs b/bindings/ergo-lib-c/src/block_header.rs
index 2ca36fe1f..0cb955c29 100644
--- a/bindings/ergo-lib-c/src/block_header.rs
+++ b/bindings/ergo-lib-c/src/block_header.rs
@@ -3,8 +3,8 @@ use paste::paste;
 
 use ergo_lib_c_core::{
     block_header::{
-        block_header_from_json, block_header_id, BlockHeader, BlockHeaderPtr, BlockIdPtr,
-        ConstBlockHeaderPtr,
+        block_header_from_json, block_header_id, BlockHeader, BlockHeaderPtr, BlockId, BlockIdPtr,
+        ConstBlockHeaderPtr, ConstBlockIdPtr,
     },
     Error,
 };
@@ -47,3 +47,5 @@ pub unsafe extern "C" fn ergo_lib_block_id_delete(ptr: BlockIdPtr) {
 
 make_collection!(BlockHeaders, BlockHeader);
 make_ffi_eq!(BlockHeader);
+make_collection!(BlockIds, BlockId);
+make_ffi_eq!(BlockId);
diff --git a/bindings/ergo-lib-c/src/nipopow.rs b/bindings/ergo-lib-c/src/nipopow.rs
index e263d4fe7..307413320 100644
--- a/bindings/ergo-lib-c/src/nipopow.rs
+++ b/bindings/ergo-lib-c/src/nipopow.rs
@@ -1,12 +1,15 @@
 //! Bindings for NiPoPow
+use paste::paste;
 
 use ergo_lib_c_core::{
-    block_header::{BlockHeader, ConstBlockIdPtr},
+    block_header::{BlockHeader, BlockHeaderPtr, ConstBlockIdPtr},
     collections::CollectionPtr,
     nipopow::{
         nipopow_proof_from_json, nipopow_proof_is_better_than, nipopow_proof_to_json,
         nipopow_verifier_best_chain, nipopow_verifier_new, nipopow_verifier_process,
-        ConstNipopowProofPtr, ConstNipopowVerifierPtr, NipopowProofPtr, NipopowVerifierPtr,
+        popow_header_from_json, popow_header_get_header, popow_header_get_interlinks,
+        popow_header_to_json, ConstNipopowProofPtr, ConstNipopowVerifierPtr, ConstPoPowHeaderPtr,
+        NipopowProofPtr, NipopowVerifierPtr, PoPowHeaderPtr,
     },
     Error,
 };
@@ -15,6 +18,7 @@ use std::{
     os::raw::c_char,
 };
 
+use crate::BlockIdsPtr;
 use crate::{delete_ptr, ErrorPtr, ReturnBool};
 
 /// Implementation of the ≥ algorithm from [`KMZ17`], see Algorithm 4
@@ -106,3 +110,52 @@ pub unsafe extern "C" fn ergo_lib_nipopow_verifier_process(
 pub unsafe extern "C" fn ergo_lib_nipopow_verifier_delete(ptr: NipopowVerifierPtr) {
     delete_ptr(ptr)
 }
+
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_popow_header_from_json(
+    json_str: *const c_char,
+    header_out: *mut PoPowHeaderPtr,
+) -> ErrorPtr {
+    let json = CStr::from_ptr(json_str).to_string_lossy();
+    let res = popow_header_from_json(&json, header_out);
+    Error::c_api_from(res)
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_popow_header_to_json(
+    header: ConstPoPowHeaderPtr,
+    _json_str: *mut *const c_char,
+) -> ErrorPtr {
+    #[allow(clippy::unwrap_used)]
+    let res = match popow_header_to_json(header) {
+        Ok(s) => {
+            *_json_str = CString::new(s).unwrap().into_raw();
+            Ok(())
+        }
+        Err(e) => Err(e),
+    };
+    Error::c_api_from(res)
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_popow_header_get_interlinks(
+    popow_header: ConstPoPowHeaderPtr,
+    interlinks_out: *mut BlockIdsPtr,
+) -> ErrorPtr {
+    Error::c_api_from(popow_header_get_interlinks(popow_header, interlinks_out))
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_popow_header_get_header(
+    popow_header: ConstPoPowHeaderPtr,
+    header_out: *mut BlockHeaderPtr,
+) -> ErrorPtr {
+    Error::c_api_from(popow_header_get_header(popow_header, header_out))
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_popow_header_delete(ptr: PoPowHeaderPtr) {
+    delete_ptr(ptr)
+}
+
+make_ffi_eq!(PoPowHeader);
diff --git a/bindings/ergo-lib-ios/Sources/ErgoLib/BlockHeader.swift b/bindings/ergo-lib-ios/Sources/ErgoLib/BlockHeader.swift
index 215549da7..88cc87657 100644
--- a/bindings/ergo-lib-ios/Sources/ErgoLib/BlockHeader.swift
+++ b/bindings/ergo-lib-ios/Sources/ErgoLib/BlockHeader.swift
@@ -123,3 +123,53 @@ class BlockHeaders {
         ergo_lib_block_headers_delete(self.pointer)
     }
 }
+
+/// An ordered collection of ``BlockId``s
+class BlockIds {
+    internal var pointer: BlockIdsPtr
+
+    /// Create an empty collection
+    init() {
+        self.pointer = BlockIds.initRawPtrEmpty()
+    }
+
+    /// Takes ownership of an existing ``BlockIdsPtr``. Note: we must ensure that no other instance
+    /// of ``BlockIds`` can hold this pointer.
+    internal init(withRawPointer ptr: BlockIdsPtr) {
+        self.pointer = ptr
+    }
+
+    /// Use the C-API to create an empty collection and return the raw pointer that points to this
+    /// collection.
+    private static func initRawPtrEmpty() -> BlockIdsPtr {
+        var ptr: BlockIdsPtr?
+        ergo_lib_block_ids_new(&ptr)
+        return ptr!
+    }
+
+    /// Return the length of the collection
+    func len() -> UInt {
+        return ergo_lib_block_ids_len(self.pointer)
+    }
+
+    /// Returns the ``BlockHeader`` at location `index` if it exists.
+    func get(index: UInt) -> BlockId? {
+        var blockIdPtr: BlockIdPtr?
+        let res = ergo_lib_block_ids_get(self.pointer, index, &blockIdPtr)
+        assert(res.error == nil)
+        if res.is_some {
+            return BlockId(withRawPointer: blockIdPtr!)
+        } else {
+            return nil
+        }
+    }
+
+    /// Add a ``BlockId`` to the end of the collection.
+    func add(blockId: BlockId) {
+        ergo_lib_block_ids_add(blockId.pointer, self.pointer)
+    }
+
+    deinit {
+        ergo_lib_block_ids_delete(self.pointer)
+    }
+}
diff --git a/bindings/ergo-lib-ios/Sources/ErgoLib/Nipopow.swift b/bindings/ergo-lib-ios/Sources/ErgoLib/Nipopow.swift
index da4d608fb..3c8bb9b0f 100644
--- a/bindings/ergo-lib-ios/Sources/ErgoLib/Nipopow.swift
+++ b/bindings/ergo-lib-ios/Sources/ErgoLib/Nipopow.swift
@@ -71,3 +71,52 @@ class NipopowVerifier {
         ergo_lib_nipopow_verifier_delete(self.pointer)
     }
 }
+
+/// PoPowHeader
+class PoPowHeader {
+    internal var pointer: PoPowHeaderPtr
+    init(withJson json: String) throws {
+        var ptr: NipopowProofPtr?
+        let error = json.withCString { cs in
+            ergo_lib_popow_header_from_json(cs, &ptr)
+        }
+        try checkError(error)
+        self.pointer = ptr!
+    }
+
+    /// JSON representation as text
+    func toJSON() throws -> JSON? {
+        var cStr: UnsafePointer<CChar>?
+        let error = ergo_lib_popow_header_to_json(self.pointer, &cStr)
+        try checkError(error)
+        let str = String(cString: cStr!)
+        ergo_lib_delete_string(UnsafeMutablePointer(mutating: cStr))
+        return try str.data(using: .utf8, allowLossyConversion: false).map {
+            try JSON(data: $0)
+        }
+    }
+
+    func getHeader() throws -> BlockHeader {
+        var ptr: BlockHeaderPtr?
+        let error = ergo_lib_popow_header_get_header(self.pointer, &ptr)
+        try checkError(error)
+        return BlockHeader(withRawPointer: ptr!)
+    }
+
+    func getInterlinks() throws -> BlockIds {
+        var ptr: BlockIdsPtr?
+        let error = ergo_lib_popow_header_get_interlinks(self.pointer, &ptr)
+        try checkError(error)
+        return BlockIds(withRawPointer: ptr!)
+    }
+
+    deinit {
+        ergo_lib_popow_header_delete(self.pointer)
+    }
+}
+
+extension PoPowHeader: Equatable {
+    static func ==(lhs: PoPowHeader, rhs: PoPowHeader) -> Bool {
+        ergo_lib_po_pow_header_eq(lhs.pointer, rhs.pointer)
+    }
+}
diff --git a/bindings/ergo-lib-ios/Tests/ErgoLibTests/NipopowTests.swift b/bindings/ergo-lib-ios/Tests/ErgoLibTests/NipopowTests.swift
new file mode 100644
index 000000000..2382cd004
--- /dev/null
+++ b/bindings/ergo-lib-ios/Tests/ErgoLibTests/NipopowTests.swift
@@ -0,0 +1,58 @@
+import XCTest
+@testable import ErgoLib
+@testable import ErgoLibC
+
+final class PoPowHeaderTests: XCTestCase {
+    // Test deserialization for PoPowHeader by comparing it with manually deserializing the header and interlinks
+    func testFromJson() throws {
+        // sample PoPowHeader generated from ergo-chain-generation testing infrastructure
+        let json = """
+          {
+            "header": {
+            "version": 1,
+            "id": "06fa44cc86a2cf45312fb1587f5eed0d9611a984344446e3f21c6e18422fe334",
+            "parentId": "0000000000000000000000000000000000000000000000000000000000000000",
+            "adProofsRoot": "d6379a936c8f885b347cd3d03f068b41b7d268729843530650348cf0755fea6d",
+            "stateRoot": "000000000000000000000000000000000000000000000000000000000000000000",
+            "transactionsRoot": "024562cb92738cbe9f5345b6d78b0272f4823692d2a16a11d80459c6d61f7860",
+            "timestamp": 0,
+            "nBits": 16842752,
+            "height": 1,
+            "extensionHash": "0e5751c026e543b2e8ab2eb06099daa1d1e5df47778f7787faab45cdf12fe3a8",
+            "powSolutions": {
+            "pk": "038b0f29a60fa8d7e1aeafbe512288a6c6bc696547bbf8247db23c95e83014513c",
+            "w": "02c80663dc9fcabee47c14a17c774eefda19cbcf2ea59f2dc70c2a544358ecb56b",
+            "n": "8000000000000000",
+            "d": "106003266939236557704060319805453778767683550618649496709275559630020036173535"
+            },
+            "votes": "000000"
+            },
+            "interlinks": []
+            }
+          """;
+        let header = """
+          {
+            "version": 1,
+            "id": "06fa44cc86a2cf45312fb1587f5eed0d9611a984344446e3f21c6e18422fe334",
+            "parentId": "0000000000000000000000000000000000000000000000000000000000000000",
+            "adProofsRoot": "d6379a936c8f885b347cd3d03f068b41b7d268729843530650348cf0755fea6d",
+            "stateRoot": "000000000000000000000000000000000000000000000000000000000000000000",
+            "transactionsRoot": "024562cb92738cbe9f5345b6d78b0272f4823692d2a16a11d80459c6d61f7860",
+            "timestamp": 0,
+            "nBits": 16842752,
+            "height": 1,
+            "extensionHash": "0e5751c026e543b2e8ab2eb06099daa1d1e5df47778f7787faab45cdf12fe3a8",
+            "powSolutions": {
+            "pk": "038b0f29a60fa8d7e1aeafbe512288a6c6bc696547bbf8247db23c95e83014513c",
+            "w": "02c80663dc9fcabee47c14a17c774eefda19cbcf2ea59f2dc70c2a544358ecb56b",
+            "n": "8000000000000000",
+            "d": "106003266939236557704060319805453778767683550618649496709275559630020036173535"
+            },
+            "votes": "000000"
+           }
+        """
+        let popow = try PoPowHeader(withJson: json)
+        XCTAssert((try popow.getInterlinks()).len() == 0)
+        XCTAssert((try popow.getHeader()) == (try BlockHeader(withJson: header)))
+    }
+}
diff --git a/bindings/ergo-lib-wasm/src/nipopow.rs b/bindings/ergo-lib-wasm/src/nipopow.rs
index ed4753eb8..4f0b89339 100644
--- a/bindings/ergo-lib-wasm/src/nipopow.rs
+++ b/bindings/ergo-lib-wasm/src/nipopow.rs
@@ -53,3 +53,30 @@ impl NipopowVerifier {
         self.0.process(new_proof.0).map_err(to_js)
     }
 }
+/// PoPowHeader structure. Represents the block header and unpacked interlinks
+#[wasm_bindgen]
+#[derive(Debug, From, Into)]
+pub struct PoPowHeader(ergo_lib::ergo_nipopow::PoPowHeader);
+
+#[wasm_bindgen]
+impl PoPowHeader {
+    /// Returns block header
+    pub fn header(&self) -> BlockHeader {
+        self.0.header.clone().into()
+    }
+    /// Returns interlinks for PoPowHeader
+    pub fn interlinks(&self) -> Result<JsValue, JsError> {
+        serde_json::to_string(&self.0.interlinks)
+            .map(Into::into)
+            .map_err(Into::into)
+    }
+
+    /// Returns block height for Header
+    pub fn height(&self) -> u32 {
+        self.0.header.height
+    }
+    /// Returns Block ID for Header
+    pub fn id(&self) -> crate::block_header::BlockId {
+        self.0.header.id.clone().into()
+    }
+}
diff --git a/ergo-chain-generation/Cargo.toml b/ergo-chain-generation/Cargo.toml
index 1d21022d8..20435b8d1 100644
--- a/ergo-chain-generation/Cargo.toml
+++ b/ergo-chain-generation/Cargo.toml
@@ -15,3 +15,7 @@ num-bigint = "0.4.0"
 rand = "0.8.4"
 sigma-test-util = { version = "^0.3.0", path = "../sigma-test-util" }
 ergo-nipopow = { version = "^0.2", path = "../ergo-nipopow" }
+
+[dev-dependencies]
+sigma-ser = { version = "^0.4", path = "../sigma-ser" }
+serde_json = { version = "1.0" }
\ No newline at end of file
diff --git a/ergo-chain-generation/src/chain_generation.rs b/ergo-chain-generation/src/chain_generation.rs
index 7a63cd36b..6a83759b4 100644
--- a/ergo-chain-generation/src/chain_generation.rs
+++ b/ergo-chain-generation/src/chain_generation.rs
@@ -111,7 +111,12 @@ fn next_block(
 ) -> Option<ErgoFullBlock> {
     let interlinks = prev_block
         .as_ref()
-        .map(|b| update_interlinks(b.header.clone(), unpack_interlinks(&b.extension)))
+        .and_then(|b| {
+            Some(update_interlinks(
+                b.header.clone(),
+                unpack_interlinks(&b.extension).ok()?,
+            ))
+        })
         .unwrap_or_default();
     if !interlinks.is_empty() {
         // Only non-empty for non-genesis block
@@ -379,9 +384,11 @@ mod tests {
             },
         }))
         .take(len)
-        .map(|b| PoPowHeader {
-            header: b.header,
-            interlinks: unpack_interlinks(&b.extension),
+        .flat_map(|b| {
+            Some(PoPowHeader {
+                header: b.header,
+                interlinks: unpack_interlinks(&b.extension).ok()?,
+            })
         })
         .collect()
     }
@@ -475,4 +482,30 @@ mod tests {
         };
         assert!(proof.is_better_than(&disconnected_proof).unwrap());
     }
+
+    #[test]
+    fn test_popow_roundtrip() {
+        use sigma_ser::ScorexSerializable;
+        let size = 10;
+        let chain = generate_popowheader_chain(size, None);
+
+        for header in chain {
+            let bytes = header.scorex_serialize_bytes().unwrap();
+            assert_eq!(
+                PoPowHeader::scorex_parse(&mut std::io::Cursor::new(bytes)).unwrap(),
+                header
+            );
+        }
+    }
+
+    #[test]
+    fn test_popow_json_roundtrip() {
+        let size = 10;
+        let chain = generate_popowheader_chain(size, None);
+
+        for header in chain {
+            let json = serde_json::to_string(&header).unwrap();
+            assert_eq!(serde_json::from_str::<PoPowHeader>(&json).unwrap(), header);
+        }
+    }
 }
diff --git a/ergo-chain-generation/src/fake_pow_scheme.rs b/ergo-chain-generation/src/fake_pow_scheme.rs
index 8c12a7326..2a6ecd397 100644
--- a/ergo-chain-generation/src/fake_pow_scheme.rs
+++ b/ergo-chain-generation/src/fake_pow_scheme.rs
@@ -36,9 +36,11 @@ mod tests {
             },
         }))
         .take(len)
-        .map(|b| PoPowHeader {
-            header: b.header,
-            interlinks: unpack_interlinks(&b.extension),
+        .flat_map(|b| {
+            Some(PoPowHeader {
+                header: b.header,
+                interlinks: unpack_interlinks(&b.extension).ok()?,
+            })
         })
         .collect()
     }
@@ -66,7 +68,12 @@ mod tests {
     ) -> Option<ErgoFullBlock> {
         let interlinks = prev_block
             .as_ref()
-            .map(|b| update_interlinks(b.header.clone(), unpack_interlinks(&b.extension)))
+            .and_then(|b| {
+                Some(update_interlinks(
+                    b.header.clone(),
+                    unpack_interlinks(&b.extension).ok()?,
+                ))
+            })
             .unwrap_or_default();
         if !interlinks.is_empty() {
             // Only non-empty for non-genesis block
diff --git a/ergo-chain-generation/src/lib.rs b/ergo-chain-generation/src/lib.rs
index eaf41b3e5..921eb9723 100644
--- a/ergo-chain-generation/src/lib.rs
+++ b/ergo-chain-generation/src/lib.rs
@@ -44,6 +44,17 @@ pub struct ErgoFullBlock {
     //ad_proofs: ProofBytes,
 }
 
+impl std::convert::TryInto<ergo_nipopow::PoPowHeader> for ErgoFullBlock {
+    type Error = &'static str;
+    fn try_into(self) -> Result<ergo_nipopow::PoPowHeader, &'static str> {
+        let interlinks = unpack_interlinks(&self.extension)?;
+        Ok(ergo_nipopow::PoPowHeader {
+            header: self.header,
+            interlinks,
+        })
+    }
+}
+
 /// Extension section of Ergo block. Contains key-value storage.
 #[derive(Clone, Debug)]
 pub(crate) struct ExtensionCandidate {
@@ -54,7 +65,9 @@ pub(crate) struct ExtensionCandidate {
 static INTERLINK_VECTOR_PREFIX: u8 = 0x01;
 
 /// Unpacks interlinks from key-value format of block extension.
-pub(crate) fn unpack_interlinks(extension: &ExtensionCandidate) -> Vec<BlockId> {
+pub(crate) fn unpack_interlinks(
+    extension: &ExtensionCandidate,
+) -> Result<Vec<BlockId>, &'static str> {
     let mut res = vec![];
     let entries = extension
         .fields
@@ -64,13 +77,15 @@ pub(crate) fn unpack_interlinks(extension: &ExtensionCandidate) -> Vec<BlockId>
         // Each interlink is packed as [qty | blockId], which qty is a single-byte value
         // representing the number of duplicates of `blockId`. Every `BlockId` is 32 bytes which
         // implies that `bytes` is 33 bytes.
-        assert_eq!(bytes.len(), 33);
+        if bytes.len() != 33 {
+            return Err("Interlinks must be 33 bytes in size");
+        }
         let qty = bytes[0];
         let block_id_bytes: [u8; 32] = bytes[1..].try_into().unwrap();
         let block_id = BlockId(Digest32::from(block_id_bytes));
         res.extend(std::iter::repeat(block_id).take(qty as usize));
     }
-    res
+    Ok(res)
 }
 
 /// Packs interlinks into key-value format of the block extension.
diff --git a/ergo-nipopow/src/nipopow_proof.rs b/ergo-nipopow/src/nipopow_proof.rs
index cdd7497a3..dcb0e58fc 100644
--- a/ergo-nipopow/src/nipopow_proof.rs
+++ b/ergo-nipopow/src/nipopow_proof.rs
@@ -195,7 +195,7 @@ pub enum NipopowProofError {
 }
 
 #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
-/// Stub type until issue #489 is closed.
+/// PoPowHeader structure. Represents the block header and unpacked interlinks
 pub struct PoPowHeader {
     /// The block header
     pub header: Header,
@@ -204,13 +204,37 @@ pub struct PoPowHeader {
     pub interlinks: Vec<BlockId>,
 }
 
-#[allow(clippy::todo)]
 impl ScorexSerializable for PoPowHeader {
-    fn scorex_serialize<W: WriteSigmaVlqExt>(&self, _w: &mut W) -> ScorexSerializeResult {
-        todo!()
+    fn scorex_serialize<W: WriteSigmaVlqExt>(&self, w: &mut W) -> ScorexSerializeResult {
+        let bytes = self.header.scorex_serialize_bytes()?;
+        w.put_u32(bytes.len() as u32)?;
+        w.write_all(&bytes)?;
+        w.put_u32(self.interlinks.len() as u32)?;
+        for interlink in self.interlinks.iter() {
+            w.write_all(&*interlink.0 .0)?;
+        }
+
+        Ok(())
     }
 
-    fn scorex_parse<R: ReadSigmaVlqExt>(_r: &mut R) -> Result<Self, ScorexParsingError> {
-        todo!()
+    fn scorex_parse<R: ReadSigmaVlqExt>(r: &mut R) -> Result<Self, ScorexParsingError> {
+        let header_size = r.get_u32()?;
+        let mut buf = vec![0; header_size as usize];
+        r.read_exact(&mut buf)?;
+        let header = Header::scorex_parse(&mut std::io::Cursor::new(buf))?;
+
+        let interlinks_size = r.get_u32()?;
+
+        let interlinks: Result<Vec<BlockId>, ScorexParsingError> = (0..interlinks_size)
+            .map(|_| {
+                let mut buf = [0; 32];
+                r.read_exact(&mut buf)?;
+                Ok(BlockId(buf.into()))
+            })
+            .collect();
+        Ok(Self {
+            header,
+            interlinks: interlinks?,
+        })
     }
 }
diff --git a/ergotree-ir/src/sigma_protocol/dlog_group.rs b/ergotree-ir/src/sigma_protocol/dlog_group.rs
index 4c83535c1..eb9487be2 100644
--- a/ergotree-ir/src/sigma_protocol/dlog_group.rs
+++ b/ergotree-ir/src/sigma_protocol/dlog_group.rs
@@ -80,7 +80,7 @@ impl From<EcPoint> for String {
         #[allow(clippy::unwrap_used)]
         {
             let bytes = value.sigma_serialize_bytes().unwrap();
-            String::from_utf8(bytes).unwrap()
+            base16::encode_lower(&bytes)
         }
     }
 }


Test patch: