Saltar al contenido principal

Autenticación

El ejemplo de autenticación demuestra cómo implementar autenticación y autorización utilizando el marco de autenticación gestionado por Soroban Host.

Este ejemplo es una extensión del ejemplo de almacenamiento de datos.

Abrir en Gitpod

Ejecutar el ejemplo

Primero, pasa por el proceso de Configuración para configurar tu entorno de desarrollo, luego clona la etiqueta v21.6.0 del repositorio soroban-examples:

git clone -b v21.6.0 https://github.com/stellar/soroban-examples

O, salta la configuración del entorno de desarrollo y abre este ejemplo en Gitpod.

Para ejecutar las pruebas del ejemplo, navega al directorio auth, y usa cargo test.

cd auth
cargo test

Deberías ver la salida:

running 1 test
test test::test ... ok

Código

auth/src/lib.rs
#[contracttype]
pub enum DataKey {
Counter(Address),
}

#[contract]
pub struct IncrementContract;

#[contractimpl]
impl IncrementContract {
/// Increment increments a counter for the user, and returns the value.
pub fn increment(env: Env, user: Address, value: u32) -> u32 {
// Requires `user` to have authorized call of the `increment` of this
// contract with all the arguments passed to `increment`, i.e. `user`
// and `value`. This will panic if auth fails for any reason.
// When this is called, Soroban host performs the necessary
// authentication, manages replay prevention and enforces the user's
// authorization policies.
// The contracts normally shouldn't worry about these details and just
// write code in generic fashion using `Address` and `require_auth` (or
// `require_auth_for_args`).
user.require_auth();

// This call is equilvalent to the above:
// user.require_auth_for_args((&user, value).into_val(&env));

// The following has less arguments but is equivalent in authorization
// scope to the above calls (the user address doesn't have to be
// included in args as it's guaranteed to be authenticated).
// user.require_auth_for_args((value,).into_val(&env));

// Construct a key for the data being stored. Use an enum to set the
// contract up well for adding other types of data to be stored.
let key = DataKey::Counter(user.clone());

// Get the current count for the invoker.
let mut count: u32 = env.storage().persistent().get(&key).unwrap_or_default();

// Increment the count.
count += value;

// Save the count.
env.storage().persistent().set(&key, &count);

// Return the count to the caller.
count
}
}

Ref: https://github.com/stellar/soroban-examples/tree/v21.6.0/auth

Cómo funciona

El contrato de ejemplo almacena un contador por Address que solo puede ser incrementado por el dueño de ese Address.

Abre el archivo auth/src/lib.rs o consulta el código anterior para seguirlo.

Address

#[contracttype]
pub enum DataKey {
Counter(Address),
}

Address es un identificador universal de Soroban que puede representar una cuenta Stellar, un contrato o un 'contrato de cuenta' (un contrato que define un esquema de autenticación personalizado y políticas de autorización). Los contratos no necesitan distinguir entre estas representaciones internas. Address se puede usar cada vez que se necesite representar alguna identidad de red, como distinguir entre contadores para diferentes usuarios en este ejemplo.

Las claves de enumeración como DataKey son útiles para organizar el almacenamiento del contrato.

Diferentes valores de enumeración crean diferentes 'espacios de nombres' de claves.

En el ejemplo, el contador para cada dirección se almacena contra DataKey::Counter(Address). Si el contrato necesita comenzar a almacenar otros tipos de datos, puede hacerlo añadiendo variantes adicionales a la enumeración.

require_auth

impl IncrementContract {
pub fn increment(env: Env, user: Address, value: u32) -> u32 {
user.require_auth();

El método require_auth se puede llamar para cualquier Address. Semánticamente, user.require_auth() aquí significa 'requiere que user haya autorizado la llamada a la función increment de la instancia actual de IncrementContract con los argumentos actuales de la llamada, es decir, los valores actuales de user y value'. En términos más simples, esto asegura que el user` haya permitido incrementar el valor de su contador y que nadie más pueda incrementarlo.

Al usar require_auth, la implementación del contrato no necesita preocuparse por las firmas, la autenticación y la prevención de repetición. Todas estas características son implementadas por el host de Soroban y ocurren automáticamente siempre que se use el tipo Address.

Address tiene otro método llamado require_auth_for_args. Funciona de la misma manera que require_auth, pero permite personalizar los argumentos que deben ser autorizados. Sin embargo, esto debe usarse con cuidado para asegurar que haya un mapeo determinista entre los argumentos de invocación del contrato y los argumentos de require_auth_for_args.

Las dos llamadas siguientes son funcionalmente equivalentes a user.require_auth:

// Completely equivalent
user.require_auth_for_args((&user, value).into_val(&env));
// The following has less arguments but is equivalent in authorization
// scope to the above call (the user address doesn't have to be
// included in args as it's guaranteed to be authenticated).
user.require_auth_for_args((value,).into_val(&env));

Pruebas

Abre el archivo auth/src/test.rs para seguirlo.

auth/src/test.rs
fn test() {
let env = Env::default();
env.mock_all_auths();

let contract_id = env.register_contract(None, IncrementContract);
let client = IncrementContractClient::new(&env, &contract_id);

let user_1 = Address::random(&env);
let user_2 = Address::random(&env);

assert_eq!(client.increment(&user_1, &5), 5);
// Verify that the user indeed had to authorize a call of `increment` with
// the expected arguments:
assert_eq!(
env.auths(),
[(
// Address for which auth is performed
user_1.clone(),
// Identifier of the called contract
contract_id.clone(),
// Name of the called function
symbol_short!("increment"),
// Arguments used to call `increment` (converted to the env-managed vector via `into_val`)
(user_1.clone(), 5_u32).into_val(&env)
)]
);

// Do more `increment` calls. It's not necessary to verify authorizations
// for every one of them as we don't expect the auth logic to change from
// call to call.
assert_eq!(client.increment(&user_1, &2), 7);
assert_eq!(client.increment(&user_2, &1), 1);
assert_eq!(client.increment(&user_1, &3), 10);
assert_eq!(client.increment(&user_2, &4), 5);
}

En cualquier prueba, lo primero que siempre se requiere es un Env, que es el entorno Soroban en el que se ejecutará el contrato.

let env = Env::default();

La prueba instruye al entorno para simular todas las autenticaciones. Todas las llamadas a require_auth o require_auth_for_args tendrán éxito.

env.mock_all_auths();

El contrato se registra en el entorno usando el tipo de contrato.

let contract_id = env.register_contract(None, IncrementContract);

Todas las funciones públicas dentro de un bloque impl que está anotado con el atributo #[contractimpl] tienen una función correspondiente generada en un tipo de cliente generado. El tipo de cliente llevará el mismo nombre que el tipo de contrato con Client añadido. Por ejemplo, en nuestro contrato, el tipo de contrato es IncrementContract, y el cliente se llama IncrementContractClient.

let client = IncrementContractClient::new(&env, &contract_id);

Generar Addresses para dos usuarios. Normalmente, el valor exacto de Address no debería importar para las pruebas, por lo que se generan simplemente de forma aleatoria.

let user_1 = Address::random(&env);
let user_2 = Address::random(&env);

Invocar la función increment para user_1.

assert_eq!(client.increment(&user_1, &5), 5);

Para verificar que la(s) llamada(s) a require_auth realmente han ocurrido, usa la función auths que devuelve un vector de tuplas que contiene las autorizaciones de la más reciente invocación del contrato.

assert_eq!(
env.auths(),
[(
// Address for which auth is performed
user_1.clone(),
// Identifier of the called contract
contract_id.clone(),
// Name of the called function
symbol_short!("increment"),
// Arguments used to call `increment` (converted to the env-managed vector via `into_val`)
(user_1.clone(), 5_u32).into_val(&env)
)]
);

Invocar la función increment varias veces más para ambos usuarios. Observa que los valores se rastrean por separado para cada usuario.

assert_eq!(client.increment(&user_1, &2), 7);
assert_eq!(client.increment(&user_2, &1), 1);
assert_eq!(client.increment(&user_1, &3), 10);
assert_eq!(client.increment(&user_2, &4), 5);

Construir el contrato

Para construir el contrato en un archivo .wasm, utiliza el comando stellar contract build.

stellar contract build

El archivo .wasm debe encontrarse en el directorio target después de la construcción:

target/wasm32-unknown-unknown/release/soroban_auth_contract.wasm

Ejecutar el contrato

Si tienes stellar-cli instalado, puedes invocar funciones sobre el contrato.

Pero dado que estamos tratando con autorización y firmas, necesitamos configurar algunas identidades para usar en las pruebas y obtener sus claves públicas:

stellar keys generate acc1
stellar keys generate acc2
stellar keys address acc1
stellar keys address acc2

Salida de ejemplo con dos claves públicas de identidades:

GA6S566FD3EQDUNQ4IGSLXKW3TGVSTQW3TPHPGS7NWMCEIPBOKTNCSRU
GAJGHZ44IJXYFNOVRZGBCVKC2V62DB2KHZB7BEMYOWOLFQH4XP2TAM6B

Ahora el contrato en sí puede ser invocado. Observa que --source debe ser el nombre de la identidad que coincide con la dirección pasada al argumento --user. Esto permite que Stellar CLI firme automáticamente la carga necesaria para la invocación.

stellar contract invoke \
--source acc1 \
--wasm target/wasm32-unknown-unknown/release/soroban_auth_contract.wasm \
--id 1 \
-- \
increment \
--user GA6S566FD3EQDUNQ4IGSLXKW3TGVSTQW3TPHPGS7NWMCEIPBOKTNCSRU \
--value 2

Ejecuta unas cuantas incrementaciones más para ambas cuentas.

stellar contract invoke \
--source acc2 \
--wasm target/wasm32-unknown-unknown/release/soroban_auth_contract.wasm \
--id 1 \
-- \
increment \
--user GAJGHZ44IJXYFNOVRZGBCVKC2V62DB2KHZB7BEMYOWOLFQH4XP2TAM6B \
--value 5
stellar contract invoke \
--source acc1 \
--wasm target/wasm32-unknown-unknown/release/soroban_auth_contract.wasm \
--id 1 \
-- \
increment \
--user GA6S566FD3EQDUNQ4IGSLXKW3TGVSTQW3TPHPGS7NWMCEIPBOKTNCSRU \
--value 3
stellar contract invoke \
--source acc2 \
--wasm target/wasm32-unknown-unknown/release/soroban_auth_contract.wasm \
--id 1 \
-- \
increment \
--user GAJGHZ44IJXYFNOVRZGBCVKC2V62DB2KHZB7BEMYOWOLFQH4XP2TAM6B \
--value 10

Ver los datos que han sido almacenados para cada usuario con stellar contract read.

stellar contract read --id 1
"[""Counter"",""GA6S566FD3EQDUNQ4IGSLXKW3TGVSTQW3TPHPGS7NWMCEIPBOKTNCSRU""]",5
"[""Counter"",""GAJGHZ44IJXYFNOVRZGBCVKC2V62DB2KHZB7BEMYOWOLFQH4XP2TAM6B""]",15

También es posible previsualizar la carga de autorización que se está firmando al proporcionar la bandera --auth a la invocación:

stellar contract invoke \
--source acc2 \
--auth \
--wasm target/wasm32-unknown-unknown/release/soroban_auth_contract.wasm \
--id 1 \
-- \
increment \
--user GAJGHZ44IJXYFNOVRZGBCVKC2V62DB2KHZB7BEMYOWOLFQH4XP2TAM6B \
--value 123
Contract auth: [{"address_with_nonce":null,"root_invocation":{"contract_id":"0000000000000000000000000000000000000000000000000000000000000001","function_name":"increment","args":[{"object":{"address":{"account":{"public_key_type_ed25519":"c7bab0288753d58d3e21cc3fa68cd2546b5f78ae6635a6f1b3fe07e03ee846e9"}}}},{"u32":123}],"sub_invocations":[]},"signature_args":[]}]

Lectura adicional

Documentación de autorización proporciona más detalles sobre cómo funciona el marco de autenticación de Soroban.

Los ejemplos de Timelock y Single Offer demuestran la autorización de operaciones de tokens en nombre del usuario, que se puede extender a cualquier invocación de contrato anidada.

El ejemplo de Atomic Swap demuestra autorización multi-partes donde múltiples usuarios firman sus partes de la invocación del contrato.

El ejemplo de Custom Account demuestra un contrato de cuenta que define un esquema de autenticación personalizado y políticas de autorización definidas por el usuario.