Desarrolladores

Soroban Rust SDK v28

Author

Leigh McCulloch

Publishing date

La versión 28 del SDK de Rust de Soroban se ha publicado y contiene una variedad de cambios en el sistema de compilación, un comportamiento nuevo para admitir mejor la migración de datos de contratos y nuevas funciones. Para la mayoría de los contratos, esto debería ser una experiencia sin problemas.

  • Spec shaking v2
  • Migración de datos de contratos
  • Los eventos se publican de forma dispersa
  • Referencias ejecutables
  • Se pueden cargar contratos nativos en pruebas

Spec shaking v2

Un contrato Wasm almacena en su interior una especificación de la interfaz del contrato. Contiene una entrada por cada función, tipo, error y evento que define el contrato.

Antes de la v28, soroban-sdk decidía qué incluir a partir de los modificadores de visibilidad de Rust más un export argumento: pub los tipos y todos los eventos tenían una entrada; los no pub tipos no la tenían, y el export argumento permitía a un desarrollador elegir para un tipo. Para muchos contratos esto funcionaba lo suficientemente bien, pero para contratos complejos, contratos que importan otros contratos, o contratos más nuevos que importan bibliotecas grandes como las bibliotecas de OpenZeppelin, resultaba en grandes cantidades de tipos incluidos en la spec que el contrato no necesitaba. Si un tipo pertenece a la spec es una propiedad de la interfaz del contrato, no del módulo en el que se declara ni de su visibilidad en Rust.

Spec shaking v2 cambia la forma en que se incluyen las specs, usando uno de los mismos principios que emplean los compiladores para decidir si el código debe incluirse en el binario final: eliminación de código muerto. El SDK ahora emite una entrada para todo y el sistema de compilación en stellar-cli elimina las entradas que el contrato no usa. El mecanismo es un marcador de 14 bytes en la sección de datos de Wasm para cada tipo y evento. Existen marcadores en el código para todos los tipos, errores y eventos, pero solo se incluyen en el binario Wasm final si el contrato los usa en el límite. Los marcadores de tipos que no se usan, o que solo se usan internamente para operaciones como almacenamiento, se eliminan mediante la eliminación de código muerto de Rust. Un tipo se usa en el límite cuando se utiliza como parámetro de función, valor de retorno, en una panic_with_error! llamada, o en un evento publish(). stellar-cli busca los marcadores que sobrevivieron y quita las entradas de la spec que no tienen uno.

La característica spec shaking v2 (experimental_spec_shaking_v2) ha estado disponible por opt-in desde soroban-sdk v26, y se habilitó para cualquier contrato desarrollado con el stellar-contracts v0.7.0, por lo que una gran parte de los contratos ya se compilan con el comportamiento de la v28. En la v28, spec shaking v2 siempre está habilitado.

Hay otras dos formas en que los desarrolladores pueden esperar ver este impacto:

contractimport! produce entradas de la spec para los tipos del contrato importado. Si el contrato importador usa cualquiera de esos tipos en su interfaz, esos tipos se exportan automáticamente. Antes, los tipos de contratos importados nunca se exportaban, lo que dejaba las interfaces de contrato incompletas.

El export argumento en contracttype y contracterror se elimina. Se desaprobó en la v27 y ahora es un error de compilación.

Como la eliminación final sucede después de que se ejecuta el compilador, los contratos deben compilarse con stellar contract build desde stellar-cli v25.2.0 o superior. Compilar a Wasm con cualquier otra cosa ahora falla:

error: soroban-sdk requires stellar-cli v25.2.0+ to build a contract

La verificación solo se activa para compilaciones Wasm. Las pruebas no se ven afectadas y se pueden ejecutar con cargo test.

Migración de datos de contratos

Un #[contracttype] la estructura se representa en el libro mayor como un mapa indexado por nombres de campo. Antes de la v28, ese mapa tenía que coincidir exactamente con la estructura. Cada campo de la estructura debía estar presente como una clave, y no podía haber ninguna otra clave. Cualquier otra cosa era un error.

Eso hacía que la estructura de los datos almacenados quedara efectivamente congelada. Agregar un campo significaba que cada valor escrito por la versión anterior no se podía desempaquetar. Quitar un campo hacía lo mismo. La única manera de migrar datos era planificar con anticipación y usar estructuras versionadas o usar otras estrategias de migración complejas.

CAP-86 agregó funciones del host que desempaquetan mapas de forma dispersa, descritas en datos de contrato compatibles con la migración en el anuncio del Protocolo 28. La v28 del SDK los usa para contracttype desempaquetar.

Un campo ausente del mapa se desempaqueta como void. Void se desempaqueta en un Option como None, así que un campo agregado a una estructura como una Option se lee de vuelta como None desde datos almacenados antes de que existiera el campo.

#[contracttype]
pub struct State {
    pub count: u32,
    pub label: Option<u32>, // 👈 added after `count` was already stored
}

// A map holding only `count`, as stored before `label` existed.
let val = map![&env, (symbol_short!("count"), 5u32)].to_val();

assert_eq!(
    State::try_from_val(&env, &val),
    Ok(State { count: 5, label: None }),
);

Una clave en el mapa que no es un campo de la estructura se ignora. Un campo eliminado de una estructura se descarta al leer datos almacenados cuando el campo aún existía.

La estrategia de migración que queda disponible es:

  • Para agregar nuevos campos, agrégalos a la estructura con un Option<T> de tipo.
  • Para eliminar campos, elimínalos de la estructura.

El empaquetado no cambia. Se escribe cada campo de la estructura, incluidos Option los campos que están None, que se escriben como void.

El cambio se aplica a estructuras con campos nombrados, y a cada desempaquetado de una incluyendo lectura desde el almacenamiento y valores de entrada entrantes. Las estructuras tupla y los enums se representan como vecs, no como maps, y no se ven afectadas, al igual que contractevent y contracterror tipos.

Los eventos se publican de forma dispersa

Un evento declarado con data_format = "map", el predeterminado, publica sus campos de datos como un mapa indexado por nombres de campo. Antes se escribía cada campo, incluidos los campos cuyo valor es void, o un Option::None.

En la v28, un void o Option::None el campo se omite del mapa publicado.

#[contractevent]
pub struct Transfer {
    #[topic]
    to: Address,
    to_muxed_id: Option<u64>,
    amount: i128,
}

El evento de transferencia anterior emite solo los to y amount campos cuando to_muxed_id es Option::None.

Los eventos ahora pueden incluir campos que solo a veces son significativos, sin pagarlo en cada evento. El SEP-41 transfer evento es el caso en cuestión: un único Transfer con un opcional to_muxed_id, en lugar de dos tipos de eventos, uno con el id muxed y otro sin él. Los consumidores ven el campo cuando significa algo y no hay rastro de él cuando no.

Esto aplica solo al nivel superior del mapa de datos de un evento. El empaquetado en el resto no cambia: una contracttype struct escribe todos sus campos, ya sea que se almacene, se pase a una función o se anide dentro del valor de un campo de evento.

Un evento que deba seguir publicando cada campo puede optar por no hacerlo:

#[contractevent(sparse = false)]
pub struct Transfer {
    #[topic]
    to: Address,
    to_muxed_id: Option<u64>,
    amount: i128,
}

Referencias ejecutables

El soroban-sdk agrega soporte para referencias ejecutables que se introdujeron en CAP-85. El ejecutable de un contrato ahora puede ser una referencia a una entrada en lugar de un hash de Wasm directamente. Una entrada de referencia ejecutable es una entrada persistente, propiedad de un contrato y con clave por una etiqueta, que contiene un hash de Wasm; los contratos desplegados contra ella cargan su código desde cualquier hash que tenga en ese momento. Actualizar la entrada cambia cada contrato que la usa en su siguiente invocación, lo que hace que actualizar una flota de contratos sea una única escritura. Los contratos pueden gestionar sus propias entradas de referencia ejecutable con env.executable_refs().

Desplegar y actualizar ahora aceptan un ContractExecutable que puede ser un Wasm(hash) o ExternalRef(ContractExecutableRef { owner, tag }). Por ejemplo:

#[contractimpl]
impl Contract {
    /// Set the executable reference, keyed by `name`, pointing at
    /// `wasm_hash`.
    pub fn set_ref(env: Env, name: String, wasm_hash: BytesN<32>) {
        env.executable_refs().set(&name, &wasm_hash);
    }

    /// Deploy a contract using the executable reference entry.
    pub fn deploy(env: Env, name: String) -> Address {
        let salt = [0u8; 32];
        let deployer = env.deployer().with_current_contract(salt);
        deployer.deploy_contract(
            ContractExecutable::ExternalRef(ContractExecutableRef {
                owner: env.current_contract_address(),
                tag: name,
            }),
            (),
        )
    }
}

El ContractExecutable tipo también puede ahora ser visto por cuentas personalizadas en __check_auth. Una __check_auth función que inspecciona el ejecutable antes de aprobar un despliegue debe manejar ExternalRef. Por ejemplo:

#[contractimpl]
impl CustomAccountInterface for Account {
    type Signature = (); // Replace with the signature
    type Error = Error;

    fn __check_auth(
        env: Env,
        signature_payload: Hash<32>,
        signature: (),
        auth_contexts: Vec<Context>,
    ) -> Result<(), Error> {
        // ... verify the signature ...
        for context in auth_contexts.iter() {
            let executable = match context {
                Context::Contract(_) => continue,
                Context::CreateContractHostFn(c) => c.executable,
                Context::CreateContractWithCtorHostFn(c) => c.executable,
            };
            match executable {
                ContractExecutable::Wasm(_wasm_hash) => {
                    // ... consider the wasm hash ...
                }
                ContractExecutable::ExternalRef(_external_ref) => {
                    // ... consider the external ref ...
                }
            }
        }
        Ok(())
    }
}

Los contratos nativos pueden cargarse en pruebas

La Env::upload se ha añadido y admite cargar un contrato definido nativamente que existe en el código Rust como si fuera un Wasm, devolviendo un hash desde el cual se puede desplegar tantas veces como sea necesario. Existe para admitir pruebas de fábricas, o cualquier contrato que despliegue desde una entrada de código ya cargada, sin compilar a Wasm primero.

// Upload in test setup.
let wasm_hash = env.upload(Contract);

// Deploy from within a contract.
let contract_a = env
    .deployer()
    .with_address(deployer.clone(), [0u8; 32])
    .deploy_contract(ContractExecutable::Wasm(wasm_hash.clone()), ());

Actualización a v28

Al actualizar a soroban-sdk v28:

  • Compila con stellar contract build.
  • Elimina cualquier export argumento en contracttype o contracterror.
  • Revisa los consumidores offchain que leen el mapa de datos de un evento, porque un campo que esperan presente con un valor vacío estará ausente para los contratos compilados con v28.
  • Revisa el código que dependía de desempaquetar un contracttype que necesitaba fallar debido a campos faltantes.
  • Revisa las cuentas personalizadas que usan __check_auth para asegurar que ContractExecutable se maneje correctamente al validar los argumentos de despliegue.

Esta publicación proporciona solo una vista general técnica. Los desarrolladores deben revisar la documentación completa de migración y probar sus contratos e integraciones antes de desplegar con Soroban Rust SDK v28.

El conjunto completo de instrucciones de migración, con ejemplos ejecutables, está en la documentación de Rust del SDK:

Para un conjunto completo de notas de la versión, consulta la página de lanzamientos del SDK:

Esta versión aún no está auditada. Para una lista de versiones auditadas del Soroban Rust SDK, consulta: