Skip to content

Instantly share code, notes, and snippets.

@glihm
Last active November 29, 2023 10:47
Show Gist options
  • Save glihm/cc83a37f549f1c4e7b3a25fd8aa193cd to your computer and use it in GitHub Desktop.
Save glihm/cc83a37f549f1c4e7b3a25fd8aa193cd to your computer and use it in GitHub Desktop.
Cairo LongString impl with examples of useful Cairo traits impl

LongString opiniated implementation

The long string implementation in cairo will be present in the compiler itself. Can be tracked here: https://github.com/orgs/starkware-libs/projects/1/views/1

But waiting for this to come, here is a demo implementation of how long string can work.

This implementation is not exhaustive, and not heavily tested. However, you can find some useful implementation of some traits from of Cairo corelib such as storageAccess, PartialEq, LegacyHash, Into and Serde.

Tested with Scarb:

scarb 0.5.0 (1b109f1f6 2023-07-03)
cairo: 2.0.0 (https://crates.io/crates/cairo-lang-compiler/2.0.0)

The implementation is in one file to allow easy copy-paste in a existing Scarb project. 👍

///! An opiniated long string implementation, waiting
///! the long string being supported by Cairo natively.

use array::{ArrayTrait, SpanTrait};
use integer::{U8IntoFelt252, U32IntoFelt252, Felt252TryIntoU32};
use option::OptionTrait;
use serde::Serde;
use starknet::{SyscallResult, StorageAccess, StorageBaseAddress};
use traits::{Into, TryInto};

/// LongString represented internally as a list of short string.
#[derive(Copy, Drop)]
struct LongString {
    // Number of felt252 (short string) used to represent the
    // entire LongString.
    len: usize,

    // Span of felt252 (short string) to be concatenated
    // to have the complete long string.
    content: Span<felt252>,
}

/// Implements PartialEq, handy to compare long strings.
impl PartialEqLongString of PartialEq<LongString> {
    ///
    fn eq(lhs: @LongString, rhs: @LongString) -> bool {
        if lhs.len != rhs.len {
            return false;
        }

        let mut i = 0_usize;
        return loop {
            if i == *lhs.len {
                break true;
            }

            if lhs.content[i] != rhs.content[i] {
                break false;
            }

            i += 1;
        };
    }

    ///
    fn ne(lhs: @LongString, rhs: @LongString) -> bool {
        if lhs.len != rhs.len {
            return true;
        }

        let mut i = 0_usize;
        return loop {
            if i == *lhs.len {
                break false;
            }

            if lhs.content[i] != rhs.content[i] {
                break true;
            }

            i += 1;
        };
    }
}

/// Initializes a LongString from a short string.
impl Felt252IntoLongString of Into<felt252, LongString> {
    ///
    fn into(self: felt252) -> LongString {
        let mut content = ArrayTrait::<felt252>::new();
        content.append(self);

        LongString {
            len: 1,
            content: content.span()
        }
    }
}

/// Initializes a LongString from Array<felt252>.
impl ArrayIntoLongString of Into<Array<felt252>, LongString> {
    ///
    fn into(self: Array<felt252>) -> LongString {
        LongString {
            len: self.len(),
            content: self.span()
        }
    }
}

/// Initializes a LongString from Span<felt252>.
impl SpanIntoLongString of Into<Span<felt252>, LongString> {
    ///
    fn into(self: Span<felt252>) -> LongString {
        LongString {
            len: self.len(),
            content: self
        }
    }
}

/// Serde implementation for LongString.
impl LongStringSerde of serde::Serde<LongString> {
    ///
    fn serialize(self: @LongString, ref output: Array<felt252>) {
        // We don't need to add the length, as the Serde
        // for Span already add the length as the first
        // element of the array.
        self.content.serialize(ref output);
    }

    ///
    fn deserialize(ref serialized: Span<felt252>) -> Option<LongString> {
        // Same here, deserializing the Span gives us the len.
        let content = Serde::<Span<felt252>>::deserialize(ref serialized)?;

        Option::Some(
            LongString {
		len: content.len(),
                content,
            }
        )
    }
}

/// LegacyHash implementation for LongString.
impl LongStringLegacyHash of hash::LegacyHash::<LongString> {
    ///
    fn hash(state: felt252, value: LongString) -> felt252 {
        let mut buf: Array<felt252> = ArrayTrait::new();
        value.serialize(ref buf);

        // Poseidon is used here on the whole span to have a unique
        // key based on the content. Several other options are available here.
        let k = poseidon::poseidon_hash_span(buf.span());
        hash::LegacyHash::hash(state, k)
    }
}

/// StorageAccess implementation for LongString.
///
/// This implementation assumes that the length of the content
/// of the long string is less that 256.
/// Indeed, the strategy used here is using the `offset` (which is a u8)
/// indicating the storage slot at the given base address.
///
/// If you don't want this limitation, you can split the span
/// into several chunks of 256, of you can adopt an other strategy
/// based on a hash computed on the value + the offset derived
/// from a base address of the Span.
impl LongStringStorageAccess of starknet::StorageAccess<LongString> {
    ///
    fn read(address_domain: u32, base: starknet::StorageBaseAddress) -> SyscallResult::<LongString> {
        let len = StorageAccess::<u32>::read(address_domain, base)?;

        let mut content: Array<felt252> = ArrayTrait::new();
        let mut offset: u8 = 1;
        loop {
            if offset.into() == len + 1 {
                break ();
            }

            match starknet::storage_read_syscall(
                address_domain,
                starknet::storage_address_from_base_and_offset(base, offset)
            ) {
                Result::Ok(r) => content.append(r),
                Result::Err(e) => panic(e)
            };

            offset += 1;
        };

        SyscallResult::Ok(LongString {
            len,
            content: content.span(),
        })
    }

    ///
    fn write(address_domain: u32, base: StorageBaseAddress, value: LongString) -> SyscallResult::<()> {
        // As mentioned above, the current implementation is limited to 256 felts.
	// And remember that the first felt is not an actual value, it's the length of the content.
        assert(value.len < 255, 'LongString too long for storage');

        StorageAccess::<u32>::write(address_domain, base, value.len)?;

        let mut offset: u8 = 1;

        loop {
            if offset.into() == value.len + 1 {
                break ();
            }

            let index = offset - 1;
            let chunk = value.content[index.into()];

            match starknet::storage_write_syscall(
                address_domain,
                starknet::storage_address_from_base_and_offset(base, offset),
                *chunk
            ) {
                Result::Ok(r) => r,
                Result::Err(e) => panic(e),
            }

            offset += 1;
        };

        SyscallResult::Ok(())
    }

    /// Not sure (yet) how this one works, taken from the compiler:
    /// https://github.com/starkware-libs/cairo/blob/main/crates/cairo-lang-starknet/src/plugin/plugin_test_data/user_defined_types#L92
    fn read_at_offset_internal(address_domain: u32, base: StorageBaseAddress, offset: u8) -> SyscallResult<LongString> {
        LongStringStorageAccess::read_at_offset_internal(address_domain, base, offset)
    }

    /// Not sure (yet) how this one works, taken from the compiler:
    /// https://github.com/starkware-libs/cairo/blob/main/crates/cairo-lang-starknet/src/plugin/plugin_test_data/user_defined_types#L92
    fn write_at_offset_internal(address_domain: u32, base: StorageBaseAddress, offset: u8, value: LongString) -> SyscallResult<()> {
        LongStringStorageAccess::write_at_offset_internal(address_domain, base, offset, value)
    }

    /// We add +1 to the length as at the first offset (0), we store the
    /// actual length.
    fn size_internal(value: LongString) -> u8 {
        value.len.try_into().unwrap() + 1_u8
    }
}

/// A simple contract to test this implementation.
#[starknet::interface]
trait IMyContract<T> {
    fn store(ref self: T, string: LongString);
    fn read(self: @T) -> LongString;
    fn store_mapping(ref self: T, key: felt252, val: LongString);
    fn read_mapping(self: @T, key: felt252) -> LongString;
}

#[starknet::contract]
mod my_contract {

    use super::{IMyContract, LongString};

    #[storage]
    struct Storage {
        a_string: LongString,
        mapping_strings: LegacyMap<felt252, LongString>,
    }

    #[external(v0)]
    impl MyContract of IMyContract<ContractState> {
        fn store(ref self: ContractState, string: LongString) {
            self.a_string.write(string);
        }

        fn read(self: @ContractState) -> LongString {
            self.a_string.read()
        }

        fn store_mapping(ref self: ContractState, key: felt252, val: LongString) {
            self.mapping_strings.write(key, val);
        }

        fn read_mapping(self: @ContractState, key: felt252) -> LongString {
            self.mapping_strings.read(key)
        }
    }
}

#[cfg(test)]
mod tests {
    use serde::Serde;
    use array::{ArrayTrait, SpanTrait};
    use traits::{TryInto, Into};
    use option::OptionTrait;
    use result::ResultTrait;
    use super::{LongString, LongStringSerde, ArrayIntoLongString, SpanIntoLongString};
    use super::{my_contract, IMyContractDispatcher, IMyContractDispatcherTrait};

    /// Deploy a test contract.
    fn deploy() -> IMyContractDispatcher {
        let mut calldata: Array<felt252> = array::ArrayTrait::new();
        let (addr, _) = starknet::deploy_syscall(
            my_contract::TEST_CLASS_HASH.try_into().unwrap(),
            0,
            calldata.span(),
            false).expect('deploy_syscall failed');

        IMyContractDispatcher { contract_address: addr }
    }

    /// Should test PartialEq.
    #[test]
    #[available_gas(2000000000)]
    fn ls_partial_eq() {
        let l1: LongString = 'abcd'.into();
        let l2: LongString = 'abcd'.into();
        assert(l1 == l2, 'PartialEq eq failed');

        let l3: LongString = 'lll'.into();
        assert(l1 != l3, 'PartialEq ne failed');
    }

    /// Should init a LongString from felt252.
    #[test]
    #[available_gas(2000000000)]
    fn ls_from_felt252() {
        let u1: LongString = 'https:...'.into();
        assert(u1.len == 1, 'll len');
        assert(u1.content.len() == 1, 'content len');
        assert(*u1.content[0] == 'https:...', 'content 0');
    }

    /// Should init a LongString from Array<felt252>.
    #[test]
    #[available_gas(2000000000)]
    fn ls_from_array_felt252() {
        let mut content = ArrayTrait::<felt252>::new();
        content.append('ipfs://bafybeigdyrzt5sfp7udm7h');
        content.append('u76uh7y26nf3efuylqabf3ocaaaaaa');
        content.append('5fbzdi');

        let u1: LongString = content.into();
        assert(u1.len == 3, 'll len');
        assert(u1.content.len() == 3, 'content len');
        assert(*u1.content[0] == 'ipfs://bafybeigdyrzt5sfp7udm7h', 'content 0');
        assert(*u1.content[1] == 'u76uh7y26nf3efuylqabf3ocaaaaaa', 'content 1');
        assert(*u1.content[2] == '5fbzdi', 'content 2');

        let mut content_empty = ArrayTrait::<felt252>::new();

        let u2: LongString = content_empty.into();
        assert(u2.len == 0, 'll len');
        assert(u2.content.len() == 0, 'content len');
    }

    /// Should serialize and deserialize a LongString.
    #[test]
    #[available_gas(2000000000)]
    fn ls_serialize_deserialize() {
        let mut content = ArrayTrait::<felt252>::new();
        content.append('hello');
        content.append('world');

        let u1: LongString = content.into();

        let mut buf = ArrayTrait::<felt252>::new();
        u1.serialize(ref buf);

        assert(buf.len() == 3, 'serialized buf len');

        assert(*buf[0] == 2, 'expected len');
        assert(*buf[1] == 'hello', 'expected item 0');
        assert(*buf[2] == 'world', 'expected item 1');

        let mut sp = buf.span();

        // Will make the test fail if deserialization fails.
        let u2 = Serde::<LongString>::deserialize(ref sp).unwrap();
    }

    /// Should store LongString into storage.
    #[test]
    #[available_gas(2000000000)]
    fn ls_contract_storage() {
        let contract = deploy();

        let ls = 'abcd'.into();
        contract.store(ls);
        assert(contract.read() == ls, 'Read failed');

        let ls_1 = 'lll111'.into();
        contract.store_mapping(1, ls_1);
        assert(contract.read_mapping(1) == ls_1, 'Read 1 failed');

        let ls_2 = 'lll222'.into();
        contract.store_mapping(2, ls_2);
        assert(contract.read_mapping(2) == ls_2, 'Read 2 failed');
    }

}

To run the unit testing:

$ scarb test

running 5 tests
test ll::tests::ls_from_felt252 ... ok
test ll::tests::ls_serialize_deserialize ... ok
test ll::tests::ls_from_array_felt252 ... ok
test ll::tests::ls_partial_eq ... ok
test ll::tests::ls_contract_storage ... ok
test result: ok. 5 passed; 0 failed; 0 ignored; 0 filtered out;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment