sRFC 00015: Interfaces

Interfaces for programs

Summary

Goal is to define a simple interface specifications for programs that avoid creating additional CPIs during execution.

  • Interfaces must be discoverable from the program elf.
  • Discovery shouldn’t require a CPI
  • Calling an interface should be as fast as calling a non interfaced program
  • Transactions should be the same size
  • There needs to be some way to add interfaces to current set of programs on solana

Implementation:

1 interface defines 1 method. So instead of a “Token” interface, there is a specific interface for the Transfer method. The interface is identified by a u128 GUID, a random number that can be generated by a dev.

Publishing Interfaces
The interface is published as in .rs files as documentation for the GUID in mod interfaces

For Example:

    /// interfaces.rs

    /// Transfers tokens from one account to another either directly or via a
    /// delegate.  If this account is associated with the native mint then equal
    /// amounts of SOL and Tokens will be transferred to the destination
    /// account.
    ///
    /// Accounts expected by this instruction:
    ///
    ///   * Single owner/delegate
    ///   0. `[writable]` The source account.
    ///   1. `[writable]` The destination account.
    ///   2. `[signer]` The source account's owner/delegate.
    ///
    ///   * Multisignature owner/delegate
    ///   0. `[writable]` The source account.
    ///   1. `[writable]` The destination account.
    ///   2. `[]` The source account's multisignature owner/delegate.
    ///   3. ..3+M `[signer]` M signer accounts.
    /// Transfer {
    ///    /// The amount of tokens to transfer.
    ///    amount: u64,
    ///},
   const TRANSFER: u128 = 0x2423423fda2344321u128;

Registering Interfaces

///program's instruction.rs
interfaces!([(interfaces::TRANSFER, interface::Instruction::U8(TokenInstruction:Transfer))])

This macro takes an array of mapping the GUID to the instruction id. The instruction type is coerced into the right format by Instruction::U8, so types of any kind of instruction index can be handled. The macro generates a segment that is included in the program ELF file that can be easily parsed from the program bytecode.

Discovering Interfaces

///implementation code
fn get_interface(program_account:: Account, guid: u128) -> Result<interface::Instruction>

This helper function finds the lookup table for the interfaces at a well defined spot in the program byte code and finds the interface instruction index.

4 Likes

Github Issues related to BTF changes that would make interfaces discoverable

1 Like

Github Issues related to BTF changes that would make interfaces discoverable

The addition of a GUID and the interfaces macro seems redundant. It also seems susceptible to type confusion security issues if an attacker manages to create two distinct function signatures with the same ID.

How about the following?

  • Adjust the compiler to generate BTF for all public and externally linked entrypoints
  • Adjust the compiler to generate BTF for all imported entrypoints
  • When generating BTF for a function, also generate BTF for all transitive types (the types of the function arguments and the return type, recursively)

The ELF format only supports the specification of one entrypoint, so we could instead signal what is public through a custom flag in the dynamic symbol table, e.g. STV_PUBLIC_ENTRYPOINT.

The Solana SDK could make this more developer-friendly through a macro annotations, like so:

// Callee
#[solana_program::entrypoint]
pub fn transfer(bla: u32) -> Result<u64, String>
// Caller
use callee::transfer;

fn bla() {
  transfer(...);
}

In both the caller and callee, the ELF of both programs would contain the BTF definitions of types

  • Result<u64, u64>
  • String
  • type of callee::transfer

The runtime would then check both types for equality before execution.

This avoids the use of GUIDs and brings it more in line with regular dynamic linking.
The drawback of this is that it uses more space, as the caller ELFs will now have to store copies of the BTF. I would expect the BTF footprint to be negligible though unless devs make excessive use of templates.

Security issues aren’t a concern because the callee never trusts the caller and has to validate all inputs.

In the BTF approach the runtime does that at link time. I generally think it’s the better option, but we need an actual design for the conventions we want programs to use. Something needs to do the dispatching from a wallet signed message string to the public entry points.

This is all already planned and even mostly implemented in LLVM: emitting BTF for a type recursively triggers BTF emission of all the referenced types - this includes function prototypes and definitions. The footprint is indeed negligible - BTF for the whole linux kernel (millions of LOC) is 4.5MB today. Also since BTF is only emitted for types reachable from public entrypoints - and not emitted for unused types - even depending on crates with a large API surface like solana_program won’t significantly impact ELF size.

For CPI, the idea is that this will work completely transparently. There’s no explicit dynamic dispatch or discovery needed. We’re planning to teach rustc, cargo and the linker to use the type info generated when compiling programs, so you’ll be able invoke a callee program just like any other function, you’ll get compile time errors if you try to misuse something etc. Compile time errors are just for improved developer experience, but obviously the runtime will still not trust the resulting bytecode.

At load time then the program runtime will resolve links, apply BTF relocations and enforce whatever security constraints can be enforced based on type info. Higher level properties that can’t be expressed via the type system will be enforced at runtime.

Having said all that, as @toly pointed out we do still need a way to invoke entrypoints from a tx, so we do need a <something> => <entrypoint> mapping. Since the names of public entrypoints will likely not be mangled - the rust mangling scheme is not stable yet and we need to interop with C and one day move programs too - could we use symbol names? If we can use symbol names then essentially we don’t have to do anything special in the runtime, we already have a symbol table so we can just lookup.

3 Likes

So with Runtime V2 something like this should work

///token.rs
///Token Transfer interface

struct Token {
   authority: Pubkey,
   balance: u64
};

trait Token {
   fn transfer(from: &mut Token, to: &mut Token, amount: u64) -> Result<()>
}

then the implementation can look like this

///mytoken.rs
///my token implementation, counts the number of balance transfers

struct MyToken {
  token: Token,
  counter: u64,
}

impl Token {
   fn transfer(from: &mut Token, to: &mut Token, amount: u64) {
     let from: &mut Account = to_account(from);
     let from: &mut MyToken = from_account(from);
     from.counter += 1;
     from.token.transfer(to, amount);
   }
}

@toly would PRv2 support dynamically dispatching this trait? This looks very interesting. How would we address (lack of) ABI stability in rustc v1.x.x?

1 Like

It should be equivalent to a global extern function in C.

Do you mean dispatching from the transaction message processor or between programs? I think the tricky part will be figuring out which token implementation gets called when more then one is present.

I think we will need to be able call the extern functions from the program object.

I’d like to suggest that interface GUID’s should be the first 128 bits of a hash of the specification detailing what accounts/data the interface expects. This convention should prevent contention/confusion over specific ID’s (ie low # ID’s).

Additionally, I’d like to note that there is a need for data interfaces. For example, for any ownable object, it should be possible to determine the owning address. In different implementations that owner may be stored at different offsets into an account. It would be a huge burden on indexers/applications to require that these offsets be found manually. Additionally, a single program may have multiple different account types, so these offsets might be different within a single program.

I propose a solution in two parts: Account discriminators and an offsets section within the ELF. The idea would be that, within the binary, each different discriminator would be followed by a list of interface-offset pairs, designating the location within each account that the interface’s data can be found.

Note: If I remember correctly, some current implementations vary account type via account size. These implementations will need to be manually grandfathered in by indexers. In fact, since these implementations already exist and are indexed, it would require minimal work. However, all future programs would need to adhere to the discriminator system.

2 Likes

if I understand correctly, it seems to me like this goes against last year’s trend of composability (since everyone has their own impl of a primitive), will contribute to chain bloat, and opens up a can of worms re: vulnerability.

  1. Why are these issues not a concern?
  2. How big is the byte code for each interface implementation (e.g., for these simple transfers)?
  3. Are there any guarantees about state transitions that can be provided beyond Rust/Move aliasing rules? For example, instead of a &mut self, can we mark only part of state as mutable for a given implementation? Also, as another example, can we provide default implementations for particular methods that cannot be overwritten.

To illustrate this last point, consider another take on your last example:

struct Token {
   authority: Pubkey,
   balance: u64
};

trait Token {
   fn transfer_hook(from: &mut Token, to: &mut Token, amount: u64) -> Result<()>
  
   #[immutable]
   fn transfer(from: &mut Token, to: &mut Token, amount: u64) -> Result<()> {
     let to_account: &mut MyToken = to_account(to);
     let from_account: &mut MyToken = from_account(from);
     from_account.token.transfer(to_account, amount);

     transfer_hook(from, to, amount)
   }
}

and

struct MyToken {
  token: Token,
  counter: u64,
}

impl Token for MyToken {
   fn transfer_hook(from: &mut Token, to: &mut Token, amount: u64) {
     let from: &mut MyToken = from_account(from)
     from.counter += 1;
   }
}

which provides the same functionality while providing a base implementation that need only be audited once.

4 Likes