Starknet Standard Interface Detection

Having the ability to check if a smart contract implements a specific interface is a very important feature that supports interoperability patterns (ERC721 transferFrom, etc…). In the Ethereum ecosystem, the ERC165 specifies the “how to”, and is widely used as one of the oldest standards. In Starknet, some implementations have somehow used this Ethereum Standard for defining interface ids (like Account implementors), for being able to implement ERCs that depend on it, but there’s a lack of a formal definition, and Cairo types have been mapped to Solidity types on the fly. In the beginning, with Cairo 0 this formalization was unpractical because the language itself was likely to change a lot, but now we have achieved certain maturity with Cairo 1, which I think allows us to define in the right way this process of interface ids generation.

I propose defining an SNIP from the ERC-165 idea, but using Cairo types instead. While in the past we’ve used these Solidity types for the interface generation (Account example):

interface IAccount {

    struct Call {
        address to;
        bytes4 selector;
        bytes data;
    }

    function supports_interface(bytes4) external;
    function is_valid_signature(bytes32, bytes32[] calldata) external;
    function __execute__(Call[] calldata) external;
    function __validate__(Call[] calldata) external;
    function __validate_declare__(bytes32) external;
}

One of the problems with this is that for example, we would obtain the same interface for a param being Span<T> or Array<T>, and we would need to define if we use bytes32 or uint256 from Solidity when we have the u256 type in Cairo.

I propose having something like this:

struct Call {
    to: ContractAddress,
    selector: u32,
    calldata: Array<felt252>
}

trait IAccount {
    fn supports_interface(u32);
    fn is_valid_signature(felt252, Array<felt252>);
    fn __execute__(Array<Call>);
    fn __validate__(Array<Call>);
    fn __validate_declare__(felt252);
}

Translated into this signatures:

functionSignatures = [
    'supports_interface(u32)',
    'is_valid_signature(felt252,Array<felt252>)',
    '__execute__(Array<(ContractAddress,u32,Array<felt252>)>)',
    '__validate__(Array<(ContractAddress,u32,Array<felt252>)>)',
    '__validate_declare__(felt252)'
]

Being the interface id the XOR of them (similar to Solidity), that can be computed with this Python script as an example:

# pip install pysha3
import sha3

keccak_256 = sha3.keccak_256
functions = [
    'supports_interface(u32)',
    'is_valid_signature(felt252,Array<felt252>)',
    '__execute__(Array<(ContractAddress,u32,Array<felt252>)>)',
    '__validate__(Array<(ContractAddress,u32,Array<felt252>)>)',
    '__validate_declare__(felt252)'
]

def get_id(fn_signature):
    id = keccak_256(fn_signature.encode()).hexdigest()[0:8]
    print(fn_signature, id)
    return id

def main():
    interface_id = 0x0
    for fn in functions:
        interface_id ^= int(get_id(fn), 16)
    print(hex(interface_id))

if __name__ == '__main__':
    main()

With this SNIP we would have a settled definition of how to compute interface ids for Starknet, and this will support interoperability as ERC-165 does for Ethereum.

Hey,
Regarding The Great Interface Migration
For some time account interface should both support
supportsInterface(felt) and supports_interface(u32)
Do you think that the old supportsInterface(felt) should be true only for the old interface IDs and the new one should only support the new ones that you are describing here?
Or they should both support all of them for the time being?

Hi Eric,

First of all, i think this is a great initiative. IMO it made no sense to try to be that close to ethereum.

If we are changing they way we calculate the interface id, would it also make sense to make interface if a full felt252? I don’t see the need to follow ethereum bytes4 size. I creates many collisions, some of them even accidentally. And we also use felts for methods selectors on Starknet. Also. looks like it’s compatible with the old interface in Cairo 0 which expected a felt.

On another topic. The example for the Account interface looks great, but how would it look like if we have customs Structs? I think we need to agree on a way to do those too.

First of all, i think this is a great initiative. IMO it made no sense to try to be that close to ethereum.

If we are changing they way we calculate the interface id, would it also make sense to make interface if a full felt252? I don’t see the need to follow ethereum bytes4 size. I creates many collisions, some of them even accidentally. And we also use felts for methods selectors on Starknet. Also. looks like it’s compatible with the old interface in Cairo 0 which expected a felt.

On another topic. The example for the Account interface looks great, but how would it look like if we have customs Structs? I think we need to agree on a way to do those too.

Starting with the second topic, we have Call in the example being a custom struct, my idea is to follow what Solidity does with structs, decomposing them in tuples recursively. You can see the example here:

__execute__(Array<(ContractAddress,u32,Array<felt252>)>

About the first topic, we could maintain u32 instead of felt252 just to keep “easier to handle” ids. Is easier to check 0xc5d24601 than this 0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470 right?

Maybe is more gas costly to handle u32 than felt252 though.

Do you think that the old supportsInterface(felt) should be true only for the old interface IDs and the new one should only support the new ones that you are describing here?
Or they should both support all of them for the time being?

I think both methods should behave the same, returning true for every interface the contract supports. Keep in mind that what I’m proposing is a way of standardizing how interface ids are created, but it doesn’t enforce camelCase or snake_case, so you can still have merged syntax and generate a valid interface id using this mechanism. The use of snake_case is just a convention with important implications for interoperability.

With Accounts, for example, we would maintain camelCase and snake_case, generate two ids using this standard for both interfaces, and support both numbers in both methods.

All good regarding structs then, that should work. What about Spans vs Arrays? Any idea of what to do with them? Should we consider them different?

Regarding the selectors. I see your point, you can quickly check it visually. But you can also look only at the beginning or end to achieve the same. I would still have a full felt252 because:

  • It’s consistent with starknet selectors
  • It’s safer, avoids collisions
  • Should more performant as you don’t need to check that the first bytes are all zeros. Although it should be minimal

Just my opinion :slight_smile:

Thanks @ericnordelo for the initiative.

I agree with going with felt252 to save gas. Are there any substantial benefits for using u32 that I missed?

What about Spans vs Arrays? Any idea of what to do with them?

Span and Array are different types (even when they represent similar things). The compiler will even start treating Span as an owned type soon, as you can see from this PR. I think we should handle them as different types accordingly.

Are there any substantial benefits for using u32 that I missed?

Besides shorter (aka “easier to handle”) ids I don’t think so. I agree it could be good to maintain the consistency with selectors and save some gas using felt252. Note that function selectors can’t be used for id generation though, as they do not include parameters in Starknet, and the interface id should represent input types as well.

I’d like to provide an updated example of how it will look like if we stick with felt252 for selectors

This would be interface in Cairo:

struct Call {
    to: ContractAddress,
    selector: felt252,
    calldata: Array<felt252>
}

trait IAccount {
    fn supports_interface(felt252);
    fn is_valid_signature(felt252, Array<felt252>);
    fn __execute__(Array<Call>);
    fn __validate__(Array<Call>);
    fn __validate_declare__(felt252);
}

And this is the python code that computes the interface id. Note that i updated the hash function to starknet_keccak instead of keccak_256

# pip install cairo-lang
from starkware.starknet.public.abi import starknet_keccak

functions = [
    'supports_interface(felt252)',
    'is_valid_signature(felt252,Array<felt252>)',
    '__execute__(Array<(ContractAddress,felt252,Array<felt252>)>)',
    '__validate__(Array<(ContractAddress,felt252,Array<felt252>)>)',
    '__validate_declare__(felt252)'
]


def main():
    interface_id = 0x0
    for function in functions:
        function_id = starknet_keccak(function.encode())
        print(f"{hex(function_id)} hash for {function}")
        interface_id ^= function_id
    print('IAccount ID:')
    print(hex(interface_id))


if __name__ == "__main__":
    main()

Which outputs the following ID for IAccount

0x396002e72b10861a183bd73bd37e3a27a36b685f488f45c2d3e664d0009e51c

Do you think it’s ok? Any feedback is appreciated