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

Issue:
Storage Rent
Currently `sign_tx_input` in `ergo-lib/src/wallet/signing.rs` fails when signing a transaction spending an expired box as one of its inputs. The function should check if a box is expired and set ProofBytes to empty instead of passing it to the prover. 

Also to be able to create transactions spending expired boxes following methods should be exposed in c-bindings:

`ContextExtension`:

- `ergo_lib_context_extension_get` to be able to retrieve a context extension key pair
- `ergo_lib_context_extension_set_pair` to be able to set a context extension key pair

`Constant`:

- `ergo_lib_constant_from_i16` to be able to set output index in context extension
- `ergo_lib_constant_to_i16`

`ErgoBox`:

- `ergo_lib_ergo_box_bytes_size` to be able to calculate storage rent fee for box


Discussion / hints:

Fix patch:
diff --git a/bindings/ergo-lib-c-core/src/constant.rs b/bindings/ergo-lib-c-core/src/constant.rs
index d51d45763..c8044244a 100644
--- a/bindings/ergo-lib-c-core/src/constant.rs
+++ b/bindings/ergo-lib-c-core/src/constant.rs
@@ -58,6 +58,20 @@ pub unsafe fn constant_value_to_dbg_str(constant_ptr: ConstConstantPtr) -> Resul
     Ok(s)
 }
 
+/// Create from i16 value
+pub unsafe fn constant_from_i16(value: i16, constant_out: *mut ConstantPtr) -> Result<(), Error> {
+    let constant_out = mut_ptr_as_mut(constant_out, "constant_out")?;
+    *constant_out = Box::into_raw(Box::new(Constant(value.into())));
+    Ok(())
+}
+
+/// Extract i16 value, returning error if wrong type
+pub unsafe fn constant_to_i16(constant_ptr: ConstConstantPtr) -> Result<i16, Error> {
+    let constant = const_ptr_as_ref(constant_ptr, "constant_ptr")?;
+    let i = i16::try_extract_from(constant.0.clone())?;
+    Ok(i)
+}
+
 /// Create from i32 value
 pub unsafe fn constant_from_i32(value: i32, constant_out: *mut ConstantPtr) -> Result<(), Error> {
     let constant_out = mut_ptr_as_mut(constant_out, "constant_out")?;
diff --git a/bindings/ergo-lib-c-core/src/context_extension.rs b/bindings/ergo-lib-c-core/src/context_extension.rs
index d42651706..8ca65836b 100644
--- a/bindings/ergo-lib-c-core/src/context_extension.rs
+++ b/bindings/ergo-lib-c-core/src/context_extension.rs
@@ -1,3 +1,4 @@
+use crate::constant::{ConstConstantPtr, Constant, ConstantPtr};
 use crate::{
     util::{const_ptr_as_ref, mut_ptr_as_mut},
     Error,
@@ -40,4 +41,36 @@ pub unsafe fn context_extension_keys(
     Ok(())
 }
 
-// TODO: get method (needs Constant)
+/// Get value for key or fail if key is missing
+pub unsafe fn context_extension_get(
+    context_extension_ptr: ConstContextExtensionPtr,
+    key: u8,
+    constant_out: *mut ConstantPtr,
+) -> Result<bool, Error> {
+    let context_extension = const_ptr_as_ref(context_extension_ptr, "context_extension_ptr")?;
+    let constant_out = mut_ptr_as_mut(constant_out, "constant_out")?;
+    let constant = context_extension
+        .0
+        .values
+        .get(&key)
+        .map(|c| Constant(c.clone()));
+
+    if let Some(constant) = constant {
+        *constant_out = Box::into_raw(Box::new(constant));
+        Ok(true)
+    } else {
+        Ok(false)
+    }
+}
+
+/// Set the supplied pair in the ContextExtension
+pub unsafe fn context_extension_set_pair(
+    constant_ptr: ConstConstantPtr,
+    key: u8,
+    context_extension_ptr: ContextExtensionPtr,
+) -> Result<(), Error> {
+    let constant = const_ptr_as_ref(constant_ptr, "constant_ptr")?;
+    let context_extension = mut_ptr_as_mut(context_extension_ptr, "context_extension_ptr")?;
+    context_extension.0.values.insert(key, constant.0.clone());
+    Ok(())
+}
diff --git a/bindings/ergo-lib-c-core/src/ergo_box.rs b/bindings/ergo-lib-c-core/src/ergo_box.rs
index 03dafa43d..0cd238f30 100644
--- a/bindings/ergo-lib-c-core/src/ergo_box.rs
+++ b/bindings/ergo-lib-c-core/src/ergo_box.rs
@@ -19,6 +19,7 @@
 use std::convert::{TryFrom, TryInto};
 
 use ergo_lib::ergotree_ir::chain::{self, ergo_box::NonMandatoryRegisters};
+use ergo_lib::ergotree_ir::serialization::SigmaSerializable;
 
 use crate::{
     collections::{CollectionPtr, ConstCollectionPtr},
@@ -326,6 +327,13 @@ pub unsafe fn ergo_box_to_json_eip12(ergo_box_ptr: ConstErgoBoxPtr) -> Result<St
     Ok(s)
 }
 
+/// Calculate serialized box size(in bytes)
+pub unsafe fn ergo_box_bytes_size(ergo_box_ptr: ConstErgoBoxPtr) -> Result<usize, Error> {
+    let ergo_box = const_ptr_as_ref(ergo_box_ptr, "ergo_box_ptr")?;
+    let b = ergo_box.0.sigma_serialize_bytes()?.len();
+    Ok(b)
+}
+
 /// Pair of <value, tokens> for a box
 #[derive(PartialEq, Eq, Debug, Clone)]
 pub struct ErgoBoxAssetsData(pub(crate) ergo_lib::wallet::box_selector::ErgoBoxAssetsData);
diff --git a/bindings/ergo-lib-c/src/constant.rs b/bindings/ergo-lib-c/src/constant.rs
index 6dab35522..752519cf1 100644
--- a/bindings/ergo-lib-c/src/constant.rs
+++ b/bindings/ergo-lib-c/src/constant.rs
@@ -77,6 +77,30 @@ pub unsafe extern "C" fn ergo_lib_constant_value_to_dbg_str(
     Error::c_api_from(res)
 }
 
+/// Create from i16 value
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_constant_from_i16(value: i16, constant_out: *mut ConstantPtr) {
+    #[allow(clippy::unwrap_used)]
+    constant_from_i16(value, constant_out).unwrap();
+}
+
+/// Extract i16 value, returning error if wrong type
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_constant_to_i16(
+    constant_ptr: ConstConstantPtr,
+) -> ReturnNum<i16> {
+    match constant_to_i16(constant_ptr) {
+        Ok(value) => ReturnNum {
+            value,
+            error: std::ptr::null_mut(),
+        },
+        Err(e) => ReturnNum {
+            value: 0, // Just a dummy value
+            error: Error::c_api_from(Err(e)),
+        },
+    }
+}
+
 /// Create from i32 value
 #[no_mangle]
 pub unsafe extern "C" fn ergo_lib_constant_from_i32(value: i32, constant_out: *mut ConstantPtr) {
diff --git a/bindings/ergo-lib-c/src/context_extension.rs b/bindings/ergo-lib-c/src/context_extension.rs
index a43d6ec34..2d58ed7c7 100644
--- a/bindings/ergo-lib-c/src/context_extension.rs
+++ b/bindings/ergo-lib-c/src/context_extension.rs
@@ -1,6 +1,8 @@
+use ergo_lib_c_core::constant::{ConstConstantPtr, ConstantPtr};
 use ergo_lib_c_core::context_extension::*;
+use ergo_lib_c_core::Error;
 
-use crate::delete_ptr;
+use crate::{delete_ptr, ReturnOption};
 
 /// Create new empty ContextExtension instance
 #[no_mangle]
@@ -30,6 +32,38 @@ pub unsafe extern "C" fn ergo_lib_context_extension_keys(
     context_extension_keys(context_extension_ptr, output).unwrap();
 }
 
+/// Returns constant with given key
+/// or None if key doesn't exist
+/// or error if constants parsing were failed
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_context_extension_get(
+    context_extension_ptr: ConstContextExtensionPtr,
+    key: u8,
+    constant_out: *mut ConstantPtr,
+) -> ReturnOption {
+    match context_extension_get(context_extension_ptr, key, constant_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)),
+        },
+    }
+}
+
+/// Set the supplied pair in the ContextExtension
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_context_extension_set_pair(
+    constant_ptr: ConstConstantPtr,
+    key: u8,
+    context_extension_ptr: ContextExtensionPtr,
+) {
+    #[allow(clippy::unwrap_used)]
+    context_extension_set_pair(constant_ptr, key, context_extension_ptr).unwrap()
+}
+
 /// Drop `ContextExtension`
 #[no_mangle]
 pub unsafe extern "C" fn ergo_lib_context_extension_delete(ptr: ContextExtensionPtr) {
diff --git a/bindings/ergo-lib-c/src/ergo_box.rs b/bindings/ergo-lib-c/src/ergo_box.rs
index fea4e5c53..2c7590080 100644
--- a/bindings/ergo-lib-c/src/ergo_box.rs
+++ b/bindings/ergo-lib-c/src/ergo_box.rs
@@ -341,6 +341,13 @@ pub unsafe extern "C" fn ergo_lib_ergo_box_to_json_eip12(
     Error::c_api_from(res)
 }
 
+/// Calculate serialized box size(in bytes)
+#[no_mangle]
+pub unsafe extern "C" fn ergo_lib_ergo_box_bytes_size(ergo_box_ptr: ConstErgoBoxPtr) -> usize {
+    #[allow(clippy::unwrap_used)]
+    ergo_box_bytes_size(ergo_box_ptr).unwrap()
+}
+
 /// Drop `ErgoBox`
 #[no_mangle]
 pub unsafe extern "C" fn ergo_lib_ergo_box_delete(ptr: ErgoBoxPtr) {
diff --git a/bindings/ergo-lib-c/src/lib.rs b/bindings/ergo-lib-c/src/lib.rs
index 3d181ce37..aaec33ed2 100644
--- a/bindings/ergo-lib-c/src/lib.rs
+++ b/bindings/ergo-lib-c/src/lib.rs
@@ -147,6 +147,7 @@ pub unsafe fn delete_ptr<T>(ptr: *mut T) {
 pub trait IntegerType {}
 
 impl IntegerType for u8 {}
+impl IntegerType for i16 {}
 impl IntegerType for i32 {}
 impl IntegerType for u32 {}
 impl IntegerType for i64 {}
diff --git a/ergo-lib/src/chain/transaction.rs b/ergo-lib/src/chain/transaction.rs
index f933ded67..cbb4c2e15 100644
--- a/ergo-lib/src/chain/transaction.rs
+++ b/ergo-lib/src/chain/transaction.rs
@@ -359,7 +359,7 @@ pub fn verify_tx_input_proof(
     let ctx = Rc::new(make_context(state_context, tx_context, input_idx)?);
     let verifier = TestVerifier;
     // Try spending in storage rent, if any condition is not satisfied fallback to normal script validation
-    match try_spend_storage_rent(input, state_context, &ctx) {
+    match try_spend_storage_rent(input, &input_box, state_context, &ctx) {
         Some(()) => Ok(VerificationResult {
             result: true,
             cost: 0,
diff --git a/ergo-lib/src/chain/transaction/storage_rent.rs b/ergo-lib/src/chain/transaction/storage_rent.rs
index 38e4b18d2..8a7c469b6 100644
--- a/ergo-lib/src/chain/transaction/storage_rent.rs
+++ b/ergo-lib/src/chain/transaction/storage_rent.rs
@@ -1,4 +1,5 @@
 use ergotree_interpreter::{eval::context::Context, sigma_protocol::prover::ProofBytes};
+use ergotree_ir::chain::ergo_box::ErgoBox;
 use ergotree_ir::{
     chain::ergo_box::RegisterId, mir::constant::TryExtractInto, serialization::SigmaSerializable,
 };
@@ -14,7 +15,20 @@ pub const STORAGE_EXTENSION_INDEX: u8 = i8::MAX as u8;
 
 // Attempt to spend a box with storage rent. Returns None if any of the required conditions is not met
 pub(crate) fn try_spend_storage_rent(
-    input_box: &Input,
+    input: &Input,
+    input_box: &ErgoBox,
+    state_context: &ErgoStateContext,
+    context: &Context,
+) -> Option<()> {
+    if matches!(input.spending_proof.proof, ProofBytes::Empty) {
+        return check_storage_rent_conditions(input_box, state_context, context);
+    }
+    None
+}
+
+// Checks if storage rent conditions are met. Returns None if any of the required conditions is not met
+pub(crate) fn check_storage_rent_conditions(
+    input_box: &ErgoBox,
     state_context: &ErgoStateContext,
     context: &Context,
 ) -> Option<()> {
@@ -23,7 +37,6 @@ pub(crate) fn try_spend_storage_rent(
         .height
         .checked_sub(context.self_box.creation_height)?
         >= STORAGE_PERIOD
-        && matches!(input_box.spending_proof.proof, ProofBytes::Empty)
     {
         let output_idx: i16 = context
             .extension
@@ -33,8 +46,8 @@ pub(crate) fn try_spend_storage_rent(
             .clone()
             .try_extract_into()
             .ok()?;
-        let output_candidate = context.outputs.get(output_idx as usize)?;
 
+        let output_candidate = context.outputs.get(output_idx as usize)?;
         let storage_fee = input_box.sigma_serialize_bytes().ok()?.len() as u64
             * state_context.parameters.storage_fee_factor() as u64;
         // If the box's value is less than the required storage fee, the box can be spent without any further restrictions
@@ -42,7 +55,7 @@ pub(crate) fn try_spend_storage_rent(
             return Some(());
         }
         if output_candidate.creation_height != state_context.pre_header.height
-            || *output_candidate.value.as_u64() < context.self_box.value.as_u64() - storage_fee
+            || *output_candidate.value.as_u64() < (context.self_box.value.as_u64() - storage_fee)
         {
             return None;
         }
diff --git a/ergo-lib/src/wallet/signing.rs b/ergo-lib/src/wallet/signing.rs
index 7fb17711a..b7938f3db 100644
--- a/ergo-lib/src/wallet/signing.rs
+++ b/ergo-lib/src/wallet/signing.rs
@@ -14,12 +14,13 @@ use ergotree_ir::sigma_protocol::sigma_boolean::SigmaBoolean;
 use std::rc::Rc;
 use std::sync::Arc;
 
+use crate::chain::transaction::storage_rent::check_storage_rent_conditions;
 use crate::wallet::multi_sig::TransactionHintsBag;
 use ergotree_interpreter::eval::context::Context;
 use ergotree_interpreter::eval::env::Env;
-use ergotree_interpreter::sigma_protocol::prover::Prover;
 use ergotree_interpreter::sigma_protocol::prover::ProverError;
 use ergotree_interpreter::sigma_protocol::prover::ProverResult;
+use ergotree_interpreter::sigma_protocol::prover::{ProofBytes, Prover};
 use thiserror::Error;
 
 pub use super::tx_context::TransactionContext;
@@ -202,16 +203,29 @@ pub fn sign_tx_input(
     if let Some(bag) = tx_hints {
         hints_bag = bag.all_hints_for_input(input_idx);
     }
-    prover
-        .prove(
-            &input_box.ergo_tree,
-            &Env::empty(),
-            ctx,
-            message_to_sign,
-            &hints_bag,
-        )
-        .map(|proof| Input::new(unsigned_input.box_id, proof.into()))
-        .map_err(|e| TxSigningError::ProverError(e, input_idx))
+
+    match check_storage_rent_conditions(&input_box, state_context, &ctx.clone()) {
+        // if input is storage rent set ProofBytes to empty because no proof is needed
+        Some(()) => Ok(Input::new(
+            unsigned_input.box_id,
+            ProverResult {
+                proof: ProofBytes::Empty,
+                extension: ctx.extension.clone(),
+            }
+            .into(),
+        )),
+        // if input is not storage rent use prover
+        None => prover
+            .prove(
+                &input_box.ergo_tree,
+                &Env::empty(),
+                ctx.clone(),
+                message_to_sign,
+                &hints_bag,
+            )
+            .map(|proof| Input::new(unsigned_input.box_id, proof.into()))
+            .map_err(|e| TxSigningError::ProverError(e, input_idx)),
+    }
 }
 
 #[cfg(test)]


Test patch: