Repository: stellar/rs-soroban-env
Pull request #380 (2022-08-26 22:45:43)

Add hostfns to construct BigInt from bytes/array of digits
### What

Host side support for https://github.com/stellar/rs-soroban-sdk/issues/493

### Why

[TODO: Why this change is being made. Include any context required to understand the why.]

### Known limitations

[TODO or N/A]



Patch:
diff --git a/soroban-env-common/src/env.rs b/soroban-env-common/src/env.rs
index 22bdecdc..8fd43f36 100644
--- a/soroban-env-common/src/env.rs
+++ b/soroban-env-common/src/env.rs
@@ -317,11 +317,21 @@ macro_rules! call_macro_with_all_host_functions {
                 {"L", fn bigint_sqrt(x:Object) -> Object}
                 /// Determines the fewest bits necessary to express `x`, not including the sign.
                 {"M", fn bigint_bits(x:Object) -> u64}
-                /// Outputs the BigInt's magnitude in big-endian byte order into a binary array. The sign is dropped.
+                /// Outputs the BigInt's magnitude in big-endian byte order into a byte array. The sign is dropped.
                 {"N", fn bigint_to_bytes_be(x:Object) -> Object}
-                /// Outputs the BigInt's magnitude in the requested base in big-endian digit order into a binary array.
+                /// Outputs the BigInt's magnitude in the requested base in big-endian digit order into a byte array.
                 /// The sign is dropped. Radix must be in the range 2...256.
                 {"O", fn bigint_to_radix_be(x:Object, radix:RawVal) -> Object}
+                /// Creates a BigInt from a byte array and i32 sign.
+                /// Bytes are in big-endian order. Sign is interpreted: -1 as negative, 0 as zero, 1 as positive
+                /// If sign is 0, then the input bytes are ignored and will return a BigInt of 0.
+                {"P", fn bigint_from_bytes_be(sign:RawVal, bytes:Object) -> Object}
+                /// Creates a BigInt from a byte array `buf`, an i32 sign and an u32 radix.
+                /// Each u8 of the byte array is interpreted as one digit of the number and
+                /// must therefore be less than the radix. The bytes are in big-endian byte order.
+                /// Radix must be in the range 2..=256. Sign follows same rule as in `bigint_from_bytes_be`.
+                {"Q", fn bigint_from_radix_be(sign:RawVal, buf:Object, radix:RawVal) -> Object}
+
             }
 
             mod binary "b" {
diff --git a/soroban-env-common/src/meta.rs b/soroban-env-common/src/meta.rs
index 66334123..420d94cf 100644
--- a/soroban-env-common/src/meta.rs
+++ b/soroban-env-common/src/meta.rs
@@ -56,5 +56,5 @@
 // implementations over a long period of time.
 
 soroban_env_macros::generate_env_meta_consts!(
-    interface_version: 5,
+    interface_version: 6,
 );
diff --git a/soroban-env-host/src/host.rs b/soroban-env-host/src/host.rs
index 0aa2b0ab..b3eeb3d9 100644
--- a/soroban-env-host/src/host.rs
+++ b/soroban-env-host/src/host.rs
@@ -550,14 +550,14 @@ impl Host {
                 let bi = match sbi {
                     ScBigInt::Zero => MeteredBigInt::new(self.0.budget.clone())?,
                     ScBigInt::Positive(bytes) => MeteredBigInt::from_bytes_be(
+                        self.0.budget.clone(),
                         Sign::Plus,
                         bytes.as_ref(),
-                        self.0.budget.clone(),
                     )?,
                     ScBigInt::Negative(bytes) => MeteredBigInt::from_bytes_be(
+                        self.0.budget.clone(),
                         Sign::Minus,
                         bytes.as_ref(),
-                        self.0.budget.clone(),
                     )?,
                 };
                 self.add_host_object(bi)
@@ -1588,6 +1588,35 @@ impl CheckedEnv for Host {
         Ok(self.add_host_object(sign_bytes.1)?.into())
     }
 
+    fn bigint_from_bytes_be(&self, sign: RawVal, bytes: Object) -> Result<Object, Self::Error> {
+        let s = self.bigint_sign_from_rawval(sign)?;
+        let res = self.visit_obj(bytes, |b: &Vec<u8>| {
+            MeteredBigInt::from_bytes_be(self.0.budget.clone(), s, b)
+        })?;
+        Ok(self.add_host_object(res)?.into())
+    }
+
+    fn bigint_from_radix_be(
+        &self,
+        sign: RawVal,
+        buf: Object,
+        radix: RawVal,
+    ) -> Result<Object, Self::Error> {
+        let s = self.bigint_sign_from_rawval(sign)?;
+        let r = self.bigint_radix_from_rawval(radix)?;
+        let res = self.visit_obj(buf, |b: &Vec<u8>| {
+            if r != 256 && b.iter().any(|&b| b >= r as u8) {
+                return Err(self.err(
+                    DebugError::new(ScHostFnErrorCode::InputArgsInvalid)
+                        .msg("{} contains invalid digit, must be < radix")
+                        .arg(buf.to_raw()),
+                ));
+            }
+            MeteredBigInt::from_radix_be(self.0.budget.clone(), s, b, r)
+        })?;
+        Ok(self.add_host_object(res)?.into())
+    }
+
     // Notes on metering: covered by components
     fn serialize_to_binary(&self, v: RawVal) -> Result<Object, HostError> {
         let scv = self.from_host_val(v)?;
diff --git a/soroban-env-host/src/host/conversion.rs b/soroban-env-host/src/host/conversion.rs
index 2dbcaa3e..bde53aa8 100644
--- a/soroban-env-host/src/host/conversion.rs
+++ b/soroban-env-host/src/host/conversion.rs
@@ -1,3 +1,4 @@
+use super::metered_bigint::MeteredBigInt;
 use crate::xdr::{
     Hash, LedgerKey, LedgerKeyContractData, ScBigInt, ScHostFnErrorCode, ScHostObjErrorCode,
     ScHostValErrorCode, ScObject, ScStatic, ScVal, ScVec, Uint256,
@@ -12,8 +13,6 @@ use ed25519_dalek::{PublicKey, Signature, SIGNATURE_LENGTH};
 use num_bigint::Sign;
 use sha2::{Digest, Sha256};
 
-use super::metered_bigint::MeteredBigInt;
-
 impl Host {
     // Notes on metering: free
     pub(crate) fn usize_to_u32(&self, u: usize, msg: &'static str) -> Result<u32, HostError> {
@@ -257,4 +256,42 @@ impl Host {
             })
         }
     }
+
+    pub(crate) fn bigint_sign_from_rawval(&self, sign: RawVal) -> Result<Sign, HostError> {
+        match i32::try_from(sign) {
+            Ok(s) => match s {
+                -1 => Ok(Sign::Minus),
+                0 => Ok(Sign::NoSign),
+                1 => Ok(Sign::Plus),
+                _ => Err(self.err(
+                    DebugError::new(ScHostFnErrorCode::InputArgsInvalid)
+                        .msg("invalid sign {}, expect -1, 0 or 1")
+                        .arg(sign),
+                )),
+            },
+            Err(cvt) => Err(self.err(
+                DebugError::new(ScHostFnErrorCode::InputArgsWrongType)
+                    .msg("unexpected RawVal {} for sign, need I32")
+                    .arg(sign),
+            )),
+        }
+    }
+
+    pub(crate) fn bigint_radix_from_rawval(&self, radix: RawVal) -> Result<u32, HostError> {
+        match u32::try_from(radix) {
+            Ok(r) => match r {
+                2..=256 => Ok(r),
+                _ => Err(self.err(
+                    DebugError::new(ScHostFnErrorCode::InputArgsInvalid)
+                        .msg("invalid radix {}, expected range 2..=256")
+                        .arg(radix),
+                )),
+            },
+            Err(cvt) => Err(self.err(
+                DebugError::new(ScHostFnErrorCode::InputArgsWrongType)
+                    .msg("unexpected RawVal {} for radix, need U32")
+                    .arg(radix),
+            )),
+        }
+    }
 }
diff --git a/soroban-env-host/src/host/metered_bigint.rs b/soroban-env-host/src/host/metered_bigint.rs
index f5d1b56e..0b507532 100644
--- a/soroban-env-host/src/host/metered_bigint.rs
+++ b/soroban-env-host/src/host/metered_bigint.rs
@@ -140,7 +140,7 @@ impl MeteredBigInt {
     }
 
     pub(crate) fn div(&self, other: &Self) -> Result<Self, HostError> {
-        assert!(!other.is_zero());
+        debug_assert!(!other.is_zero());
         self.charge_div_rem(cmp::max(self.bits(), other.bits()))?;
         Ok(Self {
             budget: self.budget.clone(),
@@ -149,7 +149,7 @@ impl MeteredBigInt {
     }
 
     pub(crate) fn rem(&self, other: &Self) -> Result<Self, HostError> {
-        assert!(!other.is_zero());
+        debug_assert!(!other.is_zero());
         self.charge_div_rem(cmp::max(self.bits(), other.bits()))?;
         Ok(Self {
             budget: self.budget.clone(),
@@ -301,9 +301,9 @@ impl MeteredBigInt {
     }
 
     pub(crate) fn from_bytes_be(
+        budget: Budget,
         sign: Sign,
         bytes: &[u8],
-        budget: Budget,
     ) -> Result<Self, HostError> {
         budget.charge(CostType::BigIntFromBytes, bytes.len() as u64)?;
         Ok(Self {
@@ -317,6 +317,20 @@ impl MeteredBigInt {
         Ok((&self.num).to_bytes_be())
     }
 
+    pub(crate) fn from_radix_be(
+        budget: Budget,
+        sign: Sign,
+        buf: &[u8],
+        radix: u32,
+    ) -> Result<Self, HostError> {
+        debug_assert!(2 <= radix && radix <= 256);
+        budget.charge(CostType::BigIntFromBytes, buf.len() as u64)?;
+        // TODO: proper error code "digit cannot be >= radix"
+        let num = BigInt::from_radix_be(sign, buf, radix)
+            .ok_or(Into::<HostError>::into(ScUnknownErrorCode::General))?;
+        Ok(Self { budget, num })
+    }
+
     pub(crate) fn to_radix_be(&self, r: u32) -> Result<(Sign, Vec<u8>), HostError> {
         self.charge_to_radix(self.bits())?;
         Ok((&self.num).to_radix_be(r))
@@ -334,7 +348,7 @@ impl Clone for MeteredBigInt {
 
 impl MeteredClone for MeteredBigInt {
     fn metered_clone(&self, budget: &Budget) -> Result<Self, HostError> {
-        assert!(Rc::ptr_eq(&self.budget.0, &budget.0));
+        debug_assert!(Rc::ptr_eq(&self.budget.0, &budget.0));
         self.charge_new()?;
         Ok(self.clone())
     }
@@ -350,14 +364,14 @@ impl Eq for MeteredBigInt {}
 
 impl PartialOrd for MeteredBigInt {
     fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
-        assert!(Rc::ptr_eq(&self.budget.0, &other.budget.0));
+        debug_assert!(Rc::ptr_eq(&self.budget.0, &other.budget.0));
         self.num.partial_cmp(&other.num)
     }
 }
 
 impl Ord for MeteredBigInt {
     fn cmp(&self, other: &Self) -> cmp::Ordering {
-        assert!(Rc::ptr_eq(&self.budget.0, &other.budget.0));
+        debug_assert!(Rc::ptr_eq(&self.budget.0, &other.budget.0));
         self.num.cmp(&other.num)
     }
 }


Test patch:
diff --git a/soroban-env-host/src/test/bigint.rs b/soroban-env-host/src/test/bigint.rs
index 0ceeefb2..8cb552a2 100644
--- a/soroban-env-host/src/test/bigint.rs
+++ b/soroban-env-host/src/test/bigint.rs
@@ -201,7 +201,7 @@ fn bigint_tests() -> Result<(), HostError> {
         assert_eq!(host.bigint_bits(obj_b)?, 19);
         assert_eq!(host.bigint_bits(obj_0)?, 0);
     }
-    // convert to array
+    // convert to digits
     {
         let bin = host.bigint_to_radix_be(obj_a, 10_u32.into())?;
         let digits = host.test_bin_obj(&[2, 3, 7, 4, 3, 4, 0])?;
@@ -211,5 +211,27 @@ fn bigint_tests() -> Result<(), HostError> {
         let digits2 = host.test_bin_obj(&[4, 3, 8, 7, 3, 0])?;
         assert_eq!(host.obj_cmp(bin2.into(), digits2.into())?, 0);
     }
+    // convert from digits
+    {
+        let digits = host.test_bin_obj(&[2, 3, 7, 4, 3, 4, 0])?;
+        let bi_a = host.bigint_from_radix_be(1i32.into(), digits.into(), 10u32.into())?;
+        assert_eq!(host.obj_cmp(bi_a.into(), obj_a.into())?, 0);
+
+        let digits2 = host.test_bin_obj(&[4, 3, 8, 7, 3, 0])?;
+        let sign: i32 = -1;
+        let bi_b = host.bigint_from_radix_be(sign.into(), digits2.into(), 10u32.into())?;
+        assert_eq!(host.obj_cmp(bi_b.into(), obj_b.into())?, 0);
+    }
+    // convert to and from bytes roundtrip
+    {
+        let bytes = host.bigint_to_bytes_be(obj_a)?;
+        let a_back = host.bigint_from_bytes_be(1i32.into(), bytes)?;
+        assert_eq!(host.obj_cmp(a_back.into(), obj_a.into())?, 0);
+
+        let bytes2 = host.bigint_to_bytes_be(obj_b)?;
+        let sign: i32 = -1;
+        let b_back = host.bigint_from_bytes_be(sign.into(), bytes2)?;
+        assert_eq!(host.obj_cmp(b_back.into(), obj_b.into())?, 0);
+    }
     Ok(())
 }
diff --git a/soroban-test-wasms/wasm-workspace/Cargo.lock b/soroban-test-wasms/wasm-workspace/Cargo.lock
index 8604a9dd..6efe9dd4 100644
--- a/soroban-test-wasms/wasm-workspace/Cargo.lock
+++ b/soroban-test-wasms/wasm-workspace/Cargo.lock
@@ -725,6 +725,9 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
 name = "stellar-xdr"
 version = "0.0.1"
 source = "git+https://github.com/stellar/rs-stellar-xdr?rev=f4fe3091#f4fe309136070c3d932bfcb8019ded953b842e5c"
+dependencies = [
+ "base64",
+]
 
 [[package]]
 name = "strsim"