Skip to content

Make Addr::unchecked a const fn #869

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions contracts/hackatom/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ fn do_release(deps: DepsMut, env: Env, info: MessageInfo) -> Result<Response, Ha

let mut resp = Response::new();
resp.add_attribute("action", "release");
resp.add_attribute("destination", to_addr.clone());
resp.add_attribute("destination", to_addr);
resp.add_message(BankMsg::Send {
to_address: to_addr.into(),
amount: balance,
Expand Down Expand Up @@ -496,9 +496,9 @@ mod tests {
assert_eq!(
state,
State {
verifier: Addr::unchecked(verifier),
beneficiary: Addr::unchecked(beneficiary),
funder: Addr::unchecked(creator),
verifier: Addr::unchecked(&verifier),
beneficiary: Addr::unchecked(&beneficiary),
funder: Addr::unchecked(&creator),
}
);
}
Expand Down Expand Up @@ -563,7 +563,7 @@ mod tests {
let bin_contract: &[u8] = b"my-contract";

// return the unhashed value here
let no_work_query = query_recurse(deps.as_ref(), 0, 0, contract.clone()).unwrap();
let no_work_query = query_recurse(deps.as_ref(), 0, 0, contract).unwrap();
assert_eq!(no_work_query.hashed, Binary::from(bin_contract));

// let's see if 5 hashes are done right
Expand Down
4 changes: 1 addition & 3 deletions contracts/reflect/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,7 @@ pub fn instantiate(
info: MessageInfo,
msg: InstantiateMsg,
) -> StdResult<Response<CustomMsg>> {
let state = State {
owner: info.sender.clone(),
};
let state = State { owner: info.sender };
config(deps.storage).save(&state)?;

let mut resp = Response::new();
Expand Down
2 changes: 1 addition & 1 deletion contracts/reflect/src/testing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ pub fn mock_dependencies_with_custom_querier(
contract_balance: &[Coin],
) -> OwnedDeps<MockStorage, MockApi, MockQuerier<SpecialQuery>> {
let custom_querier: MockQuerier<SpecialQuery> =
MockQuerier::new(&[(MOCK_CONTRACT_ADDR, contract_balance)])
MockQuerier::new(&[(MOCK_CONTRACT_ADDR.as_str(), contract_balance)])
.with_custom_handler(|query| SystemResult::Ok(custom_query_execute(&query)));
OwnedDeps {
storage: MockStorage::default(),
Expand Down
2 changes: 1 addition & 1 deletion contracts/reflect/tests/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ pub fn mock_dependencies_with_custom_querier(
contract_balance: &[Coin],
) -> Backend<MockApi, MockStorage, MockQuerier<SpecialQuery>> {
let custom_querier: MockQuerier<SpecialQuery> =
MockQuerier::new(&[(MOCK_CONTRACT_ADDR, contract_balance)])
MockQuerier::new(&[(MOCK_CONTRACT_ADDR.as_str(), contract_balance)])
.with_custom_handler(|query| SystemResult::Ok(custom_query_execute(query)));

Backend {
Expand Down
2 changes: 1 addition & 1 deletion contracts/staking/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -462,7 +462,7 @@ mod tests {
let can_redelegate = amount.clone();
FullDelegation {
validator: Addr::unchecked(validator_addr),
delegator: Addr::unchecked(MOCK_CONTRACT_ADDR),
delegator: MOCK_CONTRACT_ADDR,
amount,
can_redelegate,
accumulated_rewards: Vec::new(),
Expand Down
105 changes: 89 additions & 16 deletions packages/std/src/addresses.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
#![allow(deprecated)]

use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde::{de, ser, Deserialize, Deserializer, Serialize};
use std::fmt;
use std::ops::Deref;
use std::str;

use crate::binary::Binary;

Expand All @@ -23,8 +24,18 @@ use crate::binary::Binary;
/// This type is immutable. If you really need to mutate it (Really? Are you sure?), create
/// a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String`
/// instance.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash, JsonSchema)]
pub struct Addr(String);
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, JsonSchema)]
pub struct Addr(#[schemars(with = "String")] AddrInner);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The two level nesting here is for schemars output?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eaxtly. If we find a better way to ensure it gets a "string" schema, we can simplify at any time because all the internals are private.


/// The maximum allowed size for bech32 (https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki#bech32)
const ADDR_MAX_LENGTH: usize = 90;

#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to reflect on the performance changes of this. We should add some guidance on usage.

  1. Passing Addr as an argument copies 94 bytes, rather than a 4 byte pointer (like .clone() of the past version, but no heap allocations).
  2. Passing &Addr is cheap and can be restored to Addr via copy() or clone().
  3. Addr/&Addr.as_str() is cheap
  4. Addr.to_string() requires a heap allocation (not just consuming the original Addr).

With this in mind, I think we can recommend returning Addr and passing in &Addr to functions.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, if we pass Addr references to functions (which is the recommended approach in general), and only return Addr, I think this would be OK, performance/memory-wise.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like to play around with this a bit next week. The size of Addr is also my major (and only) concern here. But since we do not have huge amounts of addresses in memory at the same time and we have a well known size limit, I think it is fair to store the addresses on the stack.

We can always push them to heap by using Box<Addr>. Then we get what we had before, just without the mutability of a String implementation.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the problem you are trying to solve?
I feel this will just make my life harder and not solve the problem that motivated AddrRef

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the problem you are trying to solve?

Make using owned Addr easier, especially literals. Then in many places you suddenly get an Addr->&str conversion instead of the opposite direction.

I'm not claiming this solves all issues and it is well possible that it does not replace what AddrRef does.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make using owned Addr easier, especially literals.

I didn't have any issues there. And am afraid I will have to think way too much about unneeded copies of this struct if it happens. See #872 for another solution that provide simple literals for tests

struct AddrInner {
/// UTF-8 encoded human readable string
data: [u8; ADDR_MAX_LENGTH],
length: usize,
}

impl Addr {
/// Creates a new `Addr` instance from the given input without checking the validity
Expand All @@ -42,49 +53,111 @@ impl Addr {
/// let address = Addr::unchecked("foobar");
/// assert_eq!(address, "foobar");
/// ```
pub fn unchecked<T: Into<String>>(input: T) -> Addr {
Addr(input.into())
pub const fn unchecked(input: &str) -> Addr {
let input_bytes = input.as_bytes();
let mut data = [0u8; ADDR_MAX_LENGTH];
let mut i = 0;
while i < input_bytes.len() {
data[i] = input_bytes[i];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can buffer overflow if we don't have a length assertion for input

i += 1;
}
Comment on lines +60 to +63
Copy link
Contributor

@yihuang yihuang Apr 9, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
while i < input_bytes.len() {
data[i] = input_bytes[i];
i += 1;
}
data[..input_bytes.len()].clone_from_slice(input.as_bytes());

I think we can do this here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here we are very limited in what is available because it must be const fn compatible. We can't even rewrite the while loop to a for i in 0...input_bytes.len() loop. clone_from_slice is not a const fn, so it cannot be used.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking about optimizing it, I looked at the implementation of eg. https://doc.rust-lang.org/core/ptr/fn.copy_nonoverlapping.html but that just point to rust intrinsics, so I guess this is the best we can do

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow – seems like we can use copy_nonoverlapping directly because it became const (rust-lang/rust#79684). For some reason this is not shown in the docs yet. Will try.


Addr(AddrInner {
data,
length: input_bytes.len(),
})
}

pub fn as_bytes(&self) -> &[u8] {
&self.0.data[..self.0.length]
}

pub fn as_str(&self) -> &str {
unsafe { str::from_utf8_unchecked(self.as_bytes()) }
}
}

impl fmt::Display for Addr {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", &self.0)
write!(f, "{}", self.as_str())
}
}

impl Serialize for Addr {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: ser::Serializer,
{
serializer.serialize_str(&self.as_str())
}
}

impl<'de> Deserialize<'de> for Addr {
fn deserialize<D>(deserializer: D) -> Result<Addr, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_str(AddrVisitor)
}
}

struct AddrVisitor;

impl<'de> de::Visitor<'de> for AddrVisitor {
type Value = Addr;

fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("Human readable address string")
}

fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
if v.len() > ADDR_MAX_LENGTH {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice to assert this here.
Should we assert this in unchecked as well? Or would that violate the const fn (you could just panic there, but it would ensure nothing gets chopped off by accident)

Err(E::custom(format!(
"Input too long to be stored in an Addr. Max: {}, actual: {}",
ADDR_MAX_LENGTH,
v.len()
)))
} else {
Ok(Addr::unchecked(v))
}
}
}

impl AsRef<str> for Addr {
#[inline]
fn as_ref(&self) -> &str {
&self.0
self.as_str()
}
}

/// Implement `Addr == &str`
impl PartialEq<&str> for Addr {
fn eq(&self, rhs: &&str) -> bool {
self.0 == *rhs
self.as_str() == *rhs
}
}

/// Implement `&str == Addr`
impl PartialEq<Addr> for &str {
fn eq(&self, rhs: &Addr) -> bool {
*self == rhs.0
*self == rhs.as_str()
}
}

/// Implement `Addr == String`
impl PartialEq<String> for Addr {
fn eq(&self, rhs: &String) -> bool {
&self.0 == rhs
self.as_str() == rhs
}
}

/// Implement `String == Addr`
impl PartialEq<Addr> for String {
fn eq(&self, rhs: &Addr) -> bool {
self == &rhs.0
self == rhs.as_str()
}
}

Expand All @@ -93,25 +166,25 @@ impl PartialEq<Addr> for String {

impl From<Addr> for String {
fn from(addr: Addr) -> Self {
addr.0
addr.as_str().to_owned()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we also call this to_string? Sometimes it is nice to have explicit names (as_str, to_string) and not just from and into

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to_string is already implemented via the Display trait implementation. This implies a ToString implementation.

}
}

impl From<&Addr> for String {
fn from(addr: &Addr) -> Self {
addr.0.clone()
addr.as_str().to_owned()
}
}

impl From<Addr> for HumanAddr {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we remove these - let's not support HumanAddr with the new code (and if someone wants that can always do addr.as_str().into().

fn from(addr: Addr) -> Self {
HumanAddr(addr.0)
HumanAddr(addr.into())
}
}

impl From<&Addr> for HumanAddr {
fn from(addr: &Addr) -> Self {
HumanAddr(addr.0.clone())
HumanAddr(addr.into())
}
}

Expand Down Expand Up @@ -268,7 +341,7 @@ mod tests {
#[test]
fn addr_unchecked_works() {
let a = Addr::unchecked("123");
let aa = Addr::unchecked(String::from("123"));
let aa = Addr::unchecked("123");
let b = Addr::unchecked("be");
assert_eq!(a, aa);
assert_ne!(a, b);
Expand Down
2 changes: 1 addition & 1 deletion packages/std/src/imports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ impl Api for ExternalApi {
}

let address = unsafe { consume_string_region_written_by_vm(human) };
Ok(Addr::unchecked(address))
Ok(Addr::unchecked(&address))
}

fn secp256k1_verify(
Expand Down
12 changes: 6 additions & 6 deletions packages/std/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ use crate::storage::MemoryStorage;
use crate::traits::{Api, Querier, QuerierResult};
use crate::types::{BlockInfo, ContractInfo, Env, MessageInfo};

pub const MOCK_CONTRACT_ADDR: &str = "cosmos2contract";
pub const MOCK_CONTRACT_ADDR: Addr = Addr::unchecked("cosmos2contract");

/// All external requirements that can be injected for unit tests.
/// It sets the given balance for the contract itself, nothing else
Expand All @@ -31,7 +31,7 @@ pub fn mock_dependencies(
OwnedDeps {
storage: MockStorage::default(),
api: MockApi::default(),
querier: MockQuerier::new(&[(MOCK_CONTRACT_ADDR, contract_balance)]),
querier: MockQuerier::new(&[(MOCK_CONTRACT_ADDR.as_str(), contract_balance)]),
}
}

Expand Down Expand Up @@ -121,7 +121,7 @@ impl Api for MockApi {
let trimmed = tmp.into_iter().filter(|&x| x != 0x00).collect();
// decode UTF-8 bytes into string
let human = String::from_utf8(trimmed)?;
Ok(Addr::unchecked(human))
Ok(Addr::unchecked(&human))
}

fn secp256k1_verify(
Expand Down Expand Up @@ -191,16 +191,16 @@ pub fn mock_env() -> Env {
chain_id: "cosmos-testnet-14002".to_string(),
},
contract: ContractInfo {
address: Addr::unchecked(MOCK_CONTRACT_ADDR),
address: MOCK_CONTRACT_ADDR,
},
}
}

/// Just set sender and funds for the message.
/// This is intended for use in test code only.
pub fn mock_info(sender: &str, funds: &[Coin]) -> MessageInfo {
pub fn mock_info<S: AsRef<str>>(sender: S, funds: &[Coin]) -> MessageInfo {
MessageInfo {
sender: Addr::unchecked(sender),
sender: Addr::unchecked(sender.as_ref()),
funds: funds.to_vec(),
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/vm/src/testing/instance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ pub fn mock_instance_with_options(
if let Some(pos) = balances.iter().position(|item| item.0 == contract_address) {
balances.remove(pos);
}
balances.push((contract_address, contract_balance));
balances.push((contract_address.as_str(), contract_balance));
}

let api = if let Some(backend_error) = options.backend_error {
Expand Down
10 changes: 5 additions & 5 deletions packages/vm/src/testing/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use super::querier::MockQuerier;
use super::storage::MockStorage;
use crate::{Backend, BackendApi, BackendError, BackendResult, GasInfo};

pub const MOCK_CONTRACT_ADDR: &str = "cosmos2contract";
pub const MOCK_CONTRACT_ADDR: Addr = Addr::unchecked("cosmos2contract");
const GAS_COST_HUMANIZE: u64 = 44;
const GAS_COST_CANONICALIZE: u64 = 55;

Expand All @@ -15,7 +15,7 @@ pub fn mock_backend(contract_balance: &[Coin]) -> Backend<MockApi, MockStorage,
Backend {
api: MockApi::default(),
storage: MockStorage::default(),
querier: MockQuerier::new(&[(MOCK_CONTRACT_ADDR, contract_balance)]),
querier: MockQuerier::new(&[(MOCK_CONTRACT_ADDR.as_str(), contract_balance)]),
}
}

Expand Down Expand Up @@ -149,16 +149,16 @@ pub fn mock_env() -> Env {
chain_id: "cosmos-testnet-14002".to_string(),
},
contract: ContractInfo {
address: Addr::unchecked(MOCK_CONTRACT_ADDR),
address: MOCK_CONTRACT_ADDR,
},
}
}

/// Just set sender and funds for the message.
/// This is intended for use in test code only.
pub fn mock_info(sender: &str, funds: &[Coin]) -> MessageInfo {
pub fn mock_info<S: AsRef<str>>(sender: S, funds: &[Coin]) -> MessageInfo {
MessageInfo {
sender: Addr::unchecked(sender),
sender: Addr::unchecked(sender.as_ref()),
funds: funds.to_vec(),
}
}
Expand Down