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

Issue:
[200 SigmaUSD] Adapt SPV NiPoPow workflow to swift
Swift implementation should mirror that of #569 


Discussion / hints:
Sounds awesome! I put a bounty on this task.

Fix patch:
diff --git a/bindings/ergo-lib-c-core/src/block_header.rs b/bindings/ergo-lib-c-core/src/block_header.rs
index 371b5f50b..245310ae5 100644
--- a/bindings/ergo-lib-c-core/src/block_header.rs
+++ b/bindings/ergo-lib-c-core/src/block_header.rs
@@ -1,10 +1,12 @@
 //! Block header
 
+use std::convert::{TryFrom, TryInto};
+
 use crate::{
     error::*,
     util::{const_ptr_as_ref, mut_ptr_as_mut},
 };
-use ergo_lib::ergo_chain_types::Header;
+use ergo_lib::ergo_chain_types::{Base16DecodedBytes, Header};
 
 /// Block header
 #[derive(PartialEq, Eq, Debug, Clone)]
@@ -49,5 +51,17 @@ pub unsafe fn block_header_transactions_root(
 /// Block id
 #[derive(PartialEq, Eq, Debug, Clone)]
 pub struct BlockId(pub(crate) ergo_lib::ergo_chain_types::BlockId);
+
 pub type BlockIdPtr = *mut BlockId;
 pub type ConstBlockIdPtr = *const BlockId;
+
+/// Convert a hex string into a BlockId
+pub unsafe fn block_id_from_str(str: &str, block_id_out: *mut BlockIdPtr) -> Result<(), Error> {
+    let block_id_out = mut_ptr_as_mut(block_id_out, "block_id_out")?;
+    let bytes = Base16DecodedBytes::try_from(str.to_string())?;
+    let block_id = bytes
+        .try_into()
+        .map(|digest| BlockId(ergo_lib::ergo_chain_types::BlockId(digest)))?;
+    *block_id_out = Box::into_raw(Box::new(block_id));
+    Ok(())
+}
diff --git a/bindings/ergo-lib-c-core/src/nipopow.rs b/bindings/ergo-lib-c-core/src/nipopow.rs
index 5501e9005..7912aedb2 100644
--- a/bindings/ergo-lib-c-core/src/nipopow.rs
+++ b/bindings/ergo-lib-c-core/src/nipopow.rs
@@ -35,6 +35,17 @@ pub unsafe fn nipopow_proof_is_better_than(
     Ok(self_proof.0.is_better_than(&other_proof.0)?)
 }
 
+/// Get suffix head
+pub unsafe fn nipopow_proof_suffix_head(
+    nipopow_proof_ptr: ConstNipopowProofPtr,
+    suffix_head_out: *mut PoPowHeaderPtr,
+) -> Result<(), Error> {
+    let proof = const_ptr_as_ref(nipopow_proof_ptr, "nipopow_proof_ptr")?;
+    let suffix_head_out = mut_ptr_as_mut(suffix_head_out, "suffix_head_out")?;
+    *suffix_head_out = Box::into_raw(Box::new(PoPowHeader(proof.0.suffix_head.clone())));
+    Ok(())
+}
+
 /// Parse from JSON.
 pub unsafe fn nipopow_proof_from_json(
     json: &str,
@@ -75,6 +86,22 @@ pub unsafe fn nipopow_verifier_new(
     Ok(())
 }
 
+/// If a best proof exists, allocate a copy and store in `best_proof_out` and return `Ok(true)`.
+/// If such a proof doesn't exist return Ok(false).
+pub unsafe fn nipopow_verifier_best_proof(
+    nipopow_verifier_ptr: ConstNipopowVerifierPtr,
+    best_proof_out: *mut NipopowProofPtr,
+) -> Result<bool, Error> {
+    let verifier = const_ptr_as_ref(nipopow_verifier_ptr, "nipopow_verifier_ptr")?;
+    let best_proof_out = mut_ptr_as_mut(best_proof_out, "best_proof_out")?;
+    if let Some(proof) = verifier.0.best_proof() {
+        *best_proof_out = Box::into_raw(Box::new(NipopowProof(proof)));
+        Ok(true)
+    } else {
+        Ok(false)
+    }
+}
+
 /// Returns chain of `BlockHeader`s from the best proof.
 pub unsafe fn nipopow_verifier_best_chain(
     nipopow_verifier_ptr: ConstNipopowVerifierPtr,
diff --git a/bindings/ergo-lib-c/src/block_header.rs b/bindings/ergo-lib-c/src/block_header.rs
index 13d2428f8..c8f2ee669 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, block_header_transactions_root, BlockHeader,
-        BlockHeaderPtr, BlockId, BlockIdPtr, ConstBlockHeaderPtr, ConstBlockIdPtr,
+        block_header_from_json, block_header_id, block_header_transactions_root, block_id_from_str,
+        BlockHeader, BlockHeaderPtr, BlockId, BlockIdPtr, ConstBlockHeaderPtr, ConstBlockIdPtr,
     },
     Error,
 };
@@ -50,6 +50,17 @@ pub unsafe extern "C" fn ergo_lib_block_header_delete(ptr: BlockHeaderPtr) {
     delete_ptr(ptr)
 }
 
+/// Convert a hex string into a BlockId
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_block_id_from_str(
+    str: *const c_char,
+    tx_id_out: *mut BlockIdPtr,
+) -> ErrorPtr {
+    let str = CStr::from_ptr(str).to_string_lossy();
+    let res = block_id_from_str(&str, tx_id_out);
+    Error::c_api_from(res)
+}
+
 /// Delete `BlockId`
 #[no_mangle]
 pub unsafe extern "C" fn ergo_lib_block_id_delete(ptr: BlockIdPtr) {
diff --git a/bindings/ergo-lib-c/src/nipopow.rs b/bindings/ergo-lib-c/src/nipopow.rs
index 7ae073535..59f1ce76b 100644
--- a/bindings/ergo-lib-c/src/nipopow.rs
+++ b/bindings/ergo-lib-c/src/nipopow.rs
@@ -6,12 +6,13 @@ use ergo_lib_c_core::{
     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,
-        popow_header_check_interlinks_proof, popow_header_from_json, popow_header_get_header,
-        popow_header_get_interlinks, popow_header_get_interlinks_proof, popow_header_to_json,
-        ConstNipopowProofPtr, ConstNipopowVerifierPtr, ConstPoPowHeaderPtr, NipopowProofPtr,
-        NipopowVerifierPtr, PoPowHeaderPtr,
+        nipopow_proof_from_json, nipopow_proof_is_better_than, nipopow_proof_suffix_head,
+        nipopow_proof_to_json, nipopow_verifier_best_chain, nipopow_verifier_best_proof,
+        nipopow_verifier_new, nipopow_verifier_process, popow_header_check_interlinks_proof,
+        popow_header_from_json, popow_header_get_header, popow_header_get_interlinks,
+        popow_header_get_interlinks_proof, popow_header_to_json, ConstNipopowProofPtr,
+        ConstNipopowVerifierPtr, ConstPoPowHeaderPtr, NipopowProofPtr, NipopowVerifierPtr,
+        PoPowHeaderPtr,
     },
     Error,
 };
@@ -20,8 +21,8 @@ use std::{
     os::raw::c_char,
 };
 
-use crate::BlockIdsPtr;
 use crate::{delete_ptr, ErrorPtr, ReturnBool};
+use crate::{BlockIdsPtr, ReturnOption};
 
 /// Implementation of the ≥ algorithm from [`KMZ17`], see Algorithm 4
 ///
@@ -43,6 +44,16 @@ pub unsafe extern "C" fn ergo_lib_nipopow_proof_is_better_than(
     }
 }
 
+/// Get suffix head
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_nipopow_proof_suffix_head(
+    nipopow_proof_ptr: ConstNipopowProofPtr,
+    suffix_head_out: *mut PoPowHeaderPtr,
+) {
+    #[allow(clippy::unwrap_used)]
+    nipopow_proof_suffix_head(nipopow_proof_ptr, suffix_head_out).unwrap();
+}
+
 /// Parse from JSON.
 #[no_mangle]
 pub unsafe extern "C" fn ergo_lib_nipopow_proof_from_json(
@@ -87,6 +98,24 @@ pub unsafe extern "C" fn ergo_lib_nipopow_verifier_new(
     nipopow_verifier_new(genesis_block_id_ptr, nipopow_verifier_out).unwrap();
 }
 
+/// Returns the best proof.
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_nipopow_verifier_best_proof(
+    nipopow_verifier_ptr: ConstNipopowVerifierPtr,
+    best_proof_out: *mut NipopowProofPtr,
+) -> ReturnOption {
+    match nipopow_verifier_best_proof(nipopow_verifier_ptr, best_proof_out) {
+        Ok(is_some) => ReturnOption {
+            is_some,
+            error: std::ptr::null_mut(),
+        },
+        Err(e) => ReturnOption {
+            is_some: false, // Just a dummy value
+            error: Error::c_api_from(Err(e)),
+        },
+    }
+}
+
 /// Returns chain of `BlockHeader`s from the best proof.
 #[no_mangle]
 pub unsafe extern "C" fn ergo_lib_nipopow_verifier_best_chain(
diff --git a/bindings/ergo-lib-ios/Sources/ErgoLib/BlockHeader.swift b/bindings/ergo-lib-ios/Sources/ErgoLib/BlockHeader.swift
index d529644b8..5d96f8038 100644
--- a/bindings/ergo-lib-ios/Sources/ErgoLib/BlockHeader.swift
+++ b/bindings/ergo-lib-ios/Sources/ErgoLib/BlockHeader.swift
@@ -49,6 +49,16 @@ class BlockHeader: FromRawPtr {
 class BlockId {
     internal var pointer: BlockIdPtr
     
+    /// Create instance from hex-encoded string
+    init(withString str: String) throws {
+        var ptr: BlockIdPtr?
+        let error = str.withCString { cs in
+            ergo_lib_block_id_from_str(cs, &ptr)
+        }
+        try checkError(error)
+        self.pointer = ptr!
+    }
+
     /// Takes ownership of an existing ``BlockIdPtr``. Note: we must ensure that no other instance
     /// of ``BlockId`` can hold this pointer.
     internal init(withRawPointer ptr: BlockIdPtr) {
@@ -66,6 +76,12 @@ extension BlockHeader: Equatable {
     }
 }
 
+extension BlockId: Equatable {
+    static func ==(lhs: BlockId, rhs: BlockId) -> Bool {
+        ergo_lib_block_id_eq(lhs.pointer, rhs.pointer)
+    }
+}
+
 /// An ordered collection of ``BlockHeader``s
 class BlockHeaders {
     internal var pointer: BlockHeadersPtr
diff --git a/bindings/ergo-lib-ios/Sources/ErgoLib/Nipopow.swift b/bindings/ergo-lib-ios/Sources/ErgoLib/Nipopow.swift
index 70c69260d..da56a6e4f 100644
--- a/bindings/ergo-lib-ios/Sources/ErgoLib/Nipopow.swift
+++ b/bindings/ergo-lib-ios/Sources/ErgoLib/Nipopow.swift
@@ -33,6 +33,13 @@ class NipopowProof: FromRawPtr {
         return res.value
     }
     
+    /// Get suffix head
+    func suffixHead() -> PoPowHeader {
+        var ptr: PoPowHeaderPtr?
+        ergo_lib_nipopow_proof_suffix_head(self.pointer, &ptr)
+        return PoPowHeader(withRawPointer: ptr!)
+    }
+    
     /// JSON representation as text
     func toJSON() throws -> JSON? {
         var cStr: UnsafePointer<CChar>?
@@ -62,6 +69,13 @@ class NipopowVerifier {
         self.pointer = ptr!
     }
     
+    /// Returns the best `NipopowProof`.
+    func bestProof() -> NipopowProof {
+        var ptr: NipopowProofPtr?
+        ergo_lib_nipopow_verifier_best_proof(self.pointer, &ptr)
+        return NipopowProof(withRawPointer: ptr!)
+    }
+    
     /// Returns chain of `BlockHeader`s from the best proof.
     func bestChain() -> BlockHeaders {
         var ptr: BlockHeadersPtr?
@@ -92,6 +106,10 @@ class PoPowHeader {
         self.pointer = ptr!
     }
 
+    internal init(withRawPointer ptr: PoPowHeaderPtr) {
+        self.pointer = ptr
+    }
+    
     /// JSON representation as text
     func toJSON() throws -> JSON? {
         var cStr: UnsafePointer<CChar>?
diff --git a/bindings/ergo-lib-ios/Tests/ErgoLibTests/Common.swift b/bindings/ergo-lib-ios/Tests/ErgoLibTests/Common.swift
index 4a8e8ffac..4623cad3a 100644
--- a/bindings/ergo-lib-ios/Tests/ErgoLibTests/Common.swift
+++ b/bindings/ergo-lib-ios/Tests/ErgoLibTests/Common.swift
@@ -1,4 +1,6 @@
 import Foundation
+@testable import ErgoLib
+@testable import ErgoLibC
 
 // The following two functions copied from: https://stackoverflow.com/a/49890939
 
@@ -35,3 +37,15 @@ func getSeeds() -> [URL] {
         "http://213.152.106.56:9030"
     ].map { URL(string: $0)! }
 }
+
+func getNipopowProof(url: URL, headerId: BlockId) async throws -> NipopowProof? {
+    let nodeConf = try NodeConf(withAddrString: url.absoluteString)
+    let restNodeApi = try RestNodeApi()
+    let nodeInfo = try await restNodeApi.getInfoAsync(nodeConf: nodeConf)
+    if nodeInfo.isAtLeastVersion4028() {
+        let proof = try await restNodeApi.getNipopowProofByHeaderIdAsync(nodeConf: nodeConf, minChainLength: UInt32(7), suffixLen: UInt32(6), headerId: headerId)
+        return proof
+    } else {
+        return nil
+    }
+}
diff --git a/bindings/ergo-lib-ios/Tests/ErgoLibTests/RestNodeApiTests.swift b/bindings/ergo-lib-ios/Tests/ErgoLibTests/RestNodeApiTests.swift
index 10b5a717a..0dff9cf4f 100644
--- a/bindings/ergo-lib-ios/Tests/ErgoLibTests/RestNodeApiTests.swift
+++ b/bindings/ergo-lib-ios/Tests/ErgoLibTests/RestNodeApiTests.swift
@@ -100,4 +100,34 @@ final class RestNodeApiTests: XCTestCase {
         )
         XCTAssert(!peersNew.isEmpty)
     }
+    
+    func testSPVWorkflow() async throws {
+        let headerId = try BlockId(withString: "d1366f762e46b7885496aaab0c42ec2950b0422d48aec3b91f45d4d0cdeb41e5")
+        let txId = try TxId(withString: "258ddfc09b94b8313bca724de44a0d74010cab26de379be845713cc129546b78")
+        let proofs = try await withThrowingTaskGroup(of: [NipopowProof].self) { group -> [NipopowProof] in
+            group.addTask {
+                let proof = try await getNipopowProof(url: URL(string: "159.65.11.55:9053")!, headerId: headerId)!
+                return [proof]
+            }
+            group.addTask {
+                let proof = try await getNipopowProof(url: URL(string: "213.239.193.208:9053")!, headerId: headerId)!
+                return [proof]
+            }
+            return try await group.reduce(into: [NipopowProof]()) { $0 += $1 }
+        }
+        
+        let genesisBlockId = try BlockId(withString: "b0244dfc267baca974a4caee06120321562784303a8a688976ae56170e4d175b")
+        let verifier = NipopowVerifier(withGenesisBlockId: genesisBlockId)
+        try verifier.process(newProof: proofs[0])
+        try verifier.process(newProof: proofs[1])
+        let bestProof = verifier.bestProof()
+        XCTAssertEqual(try bestProof.suffixHead().getHeader().getBlockId(), headerId)
+        
+        // Now verify with 3rd node
+        let nodeConf = try NodeConf(withAddrString: "159.65.11.55:9053")
+        let restNodeApi = try RestNodeApi()
+        let header = try await restNodeApi.getHeaderAsync(nodeConf: nodeConf, blockId: headerId)
+        let merkleProof = try await restNodeApi.getBlocksHeaderIdProofForTxIdAsync(nodeConf: nodeConf, blockId: headerId, txId: txId)
+        XCTAssert(try merkleProof.valid(expected_root: header.getTransactionsRoot()))
+    }
 }


Test patch: