oasis_runtime_sdk/types/
address.rs

1//! Account address type.
2use std::{convert::TryFrom, fmt};
3
4use bech32::{Bech32, Hrp};
5use sha3::Digest;
6use thiserror::Error;
7
8use oasis_core_runtime::{
9    common::{
10        crypto::{hash::Hash, signature::PublicKey as ConsensusPublicKey},
11        namespace::Namespace,
12    },
13    consensus::address::Address as ConsensusAddress,
14};
15
16use crate::crypto::{
17    multisig,
18    signature::{ed25519, secp256k1, sr25519, PublicKey},
19};
20
21const ADDRESS_VERSION_SIZE: usize = 1;
22const ADDRESS_DATA_SIZE: usize = 20;
23const ADDRESS_SIZE: usize = ADDRESS_VERSION_SIZE + ADDRESS_DATA_SIZE;
24
25/// V0 address version.
26pub const ADDRESS_V0_VERSION: u8 = 0;
27/// V0 Ed25519 addres context (shared with consensus layer).
28pub const ADDRESS_V0_ED25519_CONTEXT: &[u8] = b"oasis-core/address: staking";
29/// V0 Secp256k1 address context.
30pub const ADDRESS_V0_SECP256K1ETH_CONTEXT: &[u8] = b"oasis-runtime-sdk/address: secp256k1eth";
31/// V0 Sr25519 address context.
32pub const ADDRESS_V0_SR25519_CONTEXT: &[u8] = b"oasis-runtime-sdk/address: sr25519";
33
34/// V0 module address context.
35pub const ADDRESS_V0_MODULE_CONTEXT: &[u8] = b"oasis-runtime-sdk/address: module";
36
37/// V0 runtime address context.
38pub const ADDRESS_RUNTIME_V0_CONTEXT: &[u8] = b"oasis-core/address: runtime";
39/// V0 runtime address version.
40pub const ADDRESS_RUNTIME_V0_VERSION: u8 = 0;
41
42/// V0 multisig address context.
43pub const ADDRESS_V0_MULTISIG_CONTEXT: &[u8] = b"oasis-runtime-sdk/address: multisig";
44
45/// Human readable part for Bech32-encoded addresses.
46pub const ADDRESS_BECH32_HRP: Hrp = Hrp::parse_unchecked("oasis");
47
48/// Information for signature-based authentication and public key-based address derivation.
49#[derive(Clone, Debug, PartialEq, Eq, cbor::Encode, cbor::Decode)]
50pub enum SignatureAddressSpec {
51    /// Ed25519 address derivation compatible with the consensus layer.
52    #[cbor(rename = "ed25519")]
53    Ed25519(ed25519::PublicKey),
54
55    /// Ethereum-compatible address derivation from Secp256k1 public keys.
56    #[cbor(rename = "secp256k1eth")]
57    Secp256k1Eth(secp256k1::PublicKey),
58
59    /// Sr25519 address derivation.
60    #[cbor(rename = "sr25519")]
61    Sr25519(sr25519::PublicKey),
62}
63
64impl SignatureAddressSpec {
65    /// Try to construct an authentication/address derivation specification from the given public
66    /// key. In case the given scheme is not supported, it returns `None`.
67    pub fn try_from_pk(pk: &PublicKey) -> Option<Self> {
68        match pk {
69            PublicKey::Ed25519(pk) => Some(Self::Ed25519(pk.clone())),
70            PublicKey::Secp256k1(pk) => Some(Self::Secp256k1Eth(pk.clone())),
71            PublicKey::Sr25519(pk) => Some(Self::Sr25519(pk.clone())),
72            _ => None,
73        }
74    }
75
76    /// Public key of the authentication/address derivation specification.
77    pub fn public_key(&self) -> PublicKey {
78        match self {
79            Self::Ed25519(pk) => PublicKey::Ed25519(pk.clone()),
80            Self::Secp256k1Eth(pk) => PublicKey::Secp256k1(pk.clone()),
81            Self::Sr25519(pk) => PublicKey::Sr25519(pk.clone()),
82        }
83    }
84}
85
86/// Error.
87#[derive(Error, Debug)]
88pub enum Error {
89    #[error("malformed address")]
90    MalformedAddress,
91}
92
93/// An account address.
94#[derive(Copy, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
95pub struct Address([u8; ADDRESS_SIZE]);
96
97impl Address {
98    /// Size of an address in bytes.
99    pub const SIZE: usize = ADDRESS_SIZE;
100
101    /// Creates a new address from a context, version and data.
102    pub fn new(ctx: &'static [u8], version: u8, data: &[u8]) -> Self {
103        let h = Hash::digest_bytes_list(&[ctx, &[version], data]);
104
105        let mut a = [0; ADDRESS_SIZE];
106        a[..ADDRESS_VERSION_SIZE].copy_from_slice(&[version]);
107        a[ADDRESS_VERSION_SIZE..].copy_from_slice(h.truncated(ADDRESS_DATA_SIZE));
108
109        Self(a)
110    }
111
112    /// Tries to create a new address from raw bytes.
113    pub fn from_bytes(data: &[u8]) -> Result<Self, Error> {
114        if data.len() != ADDRESS_SIZE {
115            return Err(Error::MalformedAddress);
116        }
117
118        let mut a = [0; ADDRESS_SIZE];
119        a.copy_from_slice(data);
120
121        Ok(Self(a))
122    }
123
124    /// Convert the address into raw bytes.
125    pub fn into_bytes(self) -> [u8; ADDRESS_SIZE] {
126        self.0
127    }
128
129    /// Creates a new address for a specific module and kind.
130    pub fn from_module(module: &str, kind: &str) -> Self {
131        Self::from_module_raw(module, kind.as_bytes())
132    }
133
134    /// Creates a new address for a specific module and raw kind.
135    pub fn from_module_raw(module: &str, kind: &[u8]) -> Self {
136        Self::new(
137            ADDRESS_V0_MODULE_CONTEXT,
138            ADDRESS_V0_VERSION,
139            &[module.as_bytes(), b".", kind].concat(),
140        )
141    }
142
143    /// Creates a new runtime address.
144    pub fn from_runtime_id(id: &Namespace) -> Self {
145        Self::new(
146            ADDRESS_RUNTIME_V0_CONTEXT,
147            ADDRESS_RUNTIME_V0_VERSION,
148            id.as_ref(),
149        )
150    }
151
152    /// Creates a new address from a public key.
153    pub fn from_sigspec(spec: &SignatureAddressSpec) -> Self {
154        match spec {
155            SignatureAddressSpec::Ed25519(pk) => Self::new(
156                ADDRESS_V0_ED25519_CONTEXT,
157                ADDRESS_V0_VERSION,
158                pk.as_bytes(),
159            ),
160            SignatureAddressSpec::Secp256k1Eth(pk) => Self::new(
161                ADDRESS_V0_SECP256K1ETH_CONTEXT,
162                ADDRESS_V0_VERSION,
163                // Use a scheme such that we can compute Secp256k1 addresses from Ethereum
164                // addresses as this makes things more interoperable.
165                &pk.to_eth_address(),
166            ),
167            SignatureAddressSpec::Sr25519(pk) => Self::new(
168                ADDRESS_V0_SR25519_CONTEXT,
169                ADDRESS_V0_VERSION,
170                pk.as_bytes(),
171            ),
172        }
173    }
174
175    /// Creates a new address from a multisig configuration.
176    pub fn from_multisig(config: multisig::Config) -> Self {
177        let config_vec = cbor::to_vec(config);
178        Self::new(ADDRESS_V0_MULTISIG_CONTEXT, ADDRESS_V0_VERSION, &config_vec)
179    }
180
181    /// Creates a new address from an Ethereum-compatible address.
182    pub fn from_eth(eth_address: &[u8]) -> Self {
183        Self::new(
184            ADDRESS_V0_SECP256K1ETH_CONTEXT,
185            ADDRESS_V0_VERSION,
186            eth_address,
187        )
188    }
189
190    /// Creates a new address from a consensus-layer Ed25519 public key.
191    ///
192    /// This is a convenience wrapper and the same result can be obtained by going via the
193    /// `from_sigspec` method using the same Ed25519 public key.
194    pub fn from_consensus_pk(pk: &ConsensusPublicKey) -> Self {
195        Self::from_bytes(ConsensusAddress::from_pk(pk).as_ref()).unwrap()
196    }
197
198    /// Tries to create a new address from Bech32-encoded string.
199    pub fn from_bech32(data: &str) -> Result<Self, Error> {
200        let (hrp, data) = bech32::decode(data).map_err(|_| Error::MalformedAddress)?;
201        if hrp != ADDRESS_BECH32_HRP {
202            return Err(Error::MalformedAddress);
203        }
204
205        Self::from_bytes(&data)
206    }
207
208    /// Converts an address to Bech32 representation.
209    pub fn to_bech32(self) -> String {
210        bech32::encode::<Bech32>(ADDRESS_BECH32_HRP, &self.0).unwrap()
211    }
212}
213
214impl AsRef<[u8]> for Address {
215    fn as_ref(&self) -> &[u8] {
216        &self.0
217    }
218}
219
220impl TryFrom<&[u8]> for Address {
221    type Error = Error;
222
223    fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
224        Self::from_bytes(bytes)
225    }
226}
227
228impl From<&'static str> for Address {
229    fn from(s: &'static str) -> Self {
230        Self::from_bech32(s).unwrap()
231    }
232}
233
234impl From<Address> for Vec<u8> {
235    fn from(a: Address) -> Self {
236        a.into_bytes().into()
237    }
238}
239
240impl fmt::LowerHex for Address {
241    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
242        for i in &self.0[..] {
243            write!(f, "{i:02x}")?;
244        }
245        Ok(())
246    }
247}
248
249impl fmt::Debug for Address {
250    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
251        write!(f, "{}", self.to_bech32())?;
252        Ok(())
253    }
254}
255
256impl fmt::Display for Address {
257    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
258        write!(f, "{}", self.to_bech32())?;
259        Ok(())
260    }
261}
262
263impl cbor::Encode for Address {
264    fn into_cbor_value(self) -> cbor::Value {
265        cbor::Value::ByteString(self.as_ref().to_vec())
266    }
267}
268
269impl cbor::Decode for Address {
270    fn try_default() -> Result<Self, cbor::DecodeError> {
271        Ok(Default::default())
272    }
273
274    fn try_from_cbor_value(value: cbor::Value) -> Result<Self, cbor::DecodeError> {
275        match value {
276            cbor::Value::ByteString(data) => {
277                Self::from_bytes(&data).map_err(|_| cbor::DecodeError::UnexpectedType)
278            }
279            _ => Err(cbor::DecodeError::UnexpectedType),
280        }
281    }
282}
283
284impl slog::Value for Address {
285    fn serialize(
286        &self,
287        _record: &slog::Record<'_>,
288        key: slog::Key,
289        serializer: &mut dyn slog::Serializer,
290    ) -> slog::Result {
291        serializer.emit_str(key, &self.to_bech32())
292    }
293}
294
295impl From<Address> for ConsensusAddress {
296    fn from(addr: Address) -> ConsensusAddress {
297        ConsensusAddress::from(&addr.0)
298    }
299}
300
301/// Generate a custom Ethereum address with proper domain separation.
302pub fn generate_custom_eth_address(domain: &str, kind: &[u8]) -> [u8; 20] {
303    sha3::Keccak256::digest(
304        [
305            &[0xFFu8] as &[u8],                  // Same as CREATE2.
306            &[0x00; 20], // Use 0x00000...00 as the creator since this will never be used.
307            b"oasis-runtime-sdk/address: ethxx", // Same as salt in CREATE2.
308            &sha3::Keccak256::digest(
309                [
310                    &[0xFEu8] as &[u8], // Use invalid bytecode.
311                    b"oasis:",
312                    domain.as_bytes(),
313                    b".",
314                    kind,
315                ]
316                .concat(),
317            ),
318        ]
319        .concat(),
320    )[32 - 20..]
321        .try_into()
322        .unwrap()
323}
324
325#[cfg(test)]
326mod test {
327    use base64::prelude::*;
328    use bech32::Bech32m;
329
330    use super::*;
331    use crate::testing::keys;
332
333    #[test]
334    fn test_address_ed25519() {
335        let spec =
336            SignatureAddressSpec::Ed25519("utrdHlX///////////////////////////////////8=".into());
337
338        let addr = Address::from_sigspec(&spec);
339        assert_eq!(
340            addr.to_bech32(),
341            "oasis1qryqqccycvckcxp453tflalujvlf78xymcdqw4vz"
342        );
343    }
344
345    #[test]
346    fn test_address_secp256k1eth() {
347        let spec = SignatureAddressSpec::Secp256k1Eth(
348            "Arra3R5V////////////////////////////////////".into(),
349        );
350
351        let addr = Address::from_sigspec(&spec);
352        assert_eq!(
353            addr.to_bech32(),
354            "oasis1qzd7akz24n6fxfhdhtk977s5857h3c6gf5583mcg"
355        );
356    }
357
358    #[test]
359    fn test_address_multisig() {
360        let config = multisig::Config {
361            signers: vec![
362                multisig::Signer {
363                    public_key: keys::alice::pk(),
364                    weight: 1,
365                },
366                multisig::Signer {
367                    public_key: keys::bob::pk(),
368                    weight: 1,
369                },
370            ],
371            threshold: 2,
372        };
373        let addr = Address::from_multisig(config);
374        assert_eq!(
375            addr,
376            Address::from_bech32("oasis1qpcprk8jxpsjxw9fadxvzrv9ln7td69yus8rmtux").unwrap(),
377        );
378    }
379
380    #[test]
381    fn test_address_try_from_bytes() {
382        let bytes_fixture = vec![42u8; ADDRESS_SIZE + 1];
383        assert_eq!(
384            Address::try_from(&bytes_fixture[0..ADDRESS_SIZE]).unwrap(),
385            Address::from_bytes(&bytes_fixture[0..ADDRESS_SIZE]).unwrap()
386        );
387        assert!(matches!(
388            Address::try_from(bytes_fixture.as_slice()).unwrap_err(),
389            Error::MalformedAddress
390        ));
391    }
392
393    #[test]
394    fn test_address_from_bech32_invalid_hrp() {
395        assert!(matches!(
396            Address::from_bech32("sisoa1qpcprk8jxpsjxw9fadxvzrv9ln7td69yus8rmtux").unwrap_err(),
397            Error::MalformedAddress,
398        ));
399    }
400
401    #[test]
402    fn test_address_from_bech32_variants() {
403        let b = vec![42u8; ADDRESS_SIZE];
404        let bech32_addr = bech32::encode::<Bech32>(ADDRESS_BECH32_HRP, &b).unwrap();
405        let bech32m_addr = bech32::encode::<Bech32m>(ADDRESS_BECH32_HRP, &b).unwrap();
406
407        assert!(
408            Address::from_bech32(&bech32_addr).is_ok(),
409            "bech32 address should be ok"
410        );
411        assert!(
412            Address::from_bech32(&bech32m_addr).is_ok(),
413            "bech32m address should be ok",
414        );
415    }
416
417    #[test]
418    fn test_address_into_consensus_address() {
419        let spec =
420            SignatureAddressSpec::Ed25519("utrdHlX///////////////////////////////////8=".into());
421        let addr = Address::from_sigspec(&spec);
422
423        let consensus_addr: ConsensusAddress = addr.into();
424        assert_eq!(addr.to_bech32(), consensus_addr.to_bech32())
425    }
426
427    #[test]
428    fn test_address_from_runtime_id() {
429        let runtime_id =
430            Namespace::from("80000000000000002aff7f6dfb62720cfd735f2b037b81572fad1b7937d826b3");
431        let addr = Address::from_runtime_id(&runtime_id);
432        assert_eq!(
433            addr.to_bech32(),
434            "oasis1qpllh99nhwzrd56px4txvl26atzgg4f3a58jzzad"
435        );
436    }
437
438    #[test]
439    fn test_address_from_module() {
440        let id: u64 = 42;
441        let addr = Address::from_module_raw("contracts", &id.to_be_bytes());
442
443        assert_eq!(
444            addr.to_bech32(),
445            "oasis1qq398yyk4wt2zxhtt8c66raynelgt6ngh5yq87xg"
446        );
447    }
448
449    #[test]
450    fn test_address_from_eth() {
451        let eth_address = hex::decode("dce075e1c39b1ae0b75d554558b6451a226ffe00").unwrap();
452        let addr = Address::from_eth(&eth_address);
453        assert_eq!(
454            addr.to_bech32(),
455            "oasis1qrk58a6j2qn065m6p06jgjyt032f7qucy5wqeqpt"
456        );
457    }
458
459    #[test]
460    fn test_address_from_consensus_pk() {
461        // Same test vector as in `test_address_ed25519`.
462        let pk: ConsensusPublicKey = BASE64_STANDARD
463            .decode("utrdHlX///////////////////////////////////8=")
464            .unwrap()
465            .into();
466
467        let addr = Address::from_consensus_pk(&pk);
468        assert_eq!(
469            addr.to_bech32(),
470            "oasis1qryqqccycvckcxp453tflalujvlf78xymcdqw4vz"
471        );
472    }
473
474    #[test]
475    fn test_address_raw() {
476        let eth_address = hex::decode("dce075e1c39b1ae0b75d554558b6451a226ffe00").unwrap();
477        let addr = Address::new(
478            ADDRESS_V0_SECP256K1ETH_CONTEXT,
479            ADDRESS_V0_VERSION,
480            &eth_address,
481        );
482        assert_eq!(
483            addr.to_bech32(),
484            "oasis1qrk58a6j2qn065m6p06jgjyt032f7qucy5wqeqpt"
485        );
486    }
487}