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.
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
#[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.
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.
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 Address
es 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.
- macOS/Linux
- Windows (PowerShell)
stellar contract invoke \
--source acc1 \
--wasm target/wasm32-unknown-unknown/release/soroban_auth_contract.wasm \
--id 1 \
-- \
increment \
--user GA6S566FD3EQDUNQ4IGSLXKW3TGVSTQW3TPHPGS7NWMCEIPBOKTNCSRU \
--value 2
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.
- macOS/Linux
- Windows (PowerShell)
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
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:
- macOS/Linux
- Windows (PowerShell)
stellar contract invoke \
--source acc2 \
--auth \
--wasm target/wasm32-unknown-unknown/release/soroban_auth_contract.wasm \
--id 1 \
-- \
increment \
--user GAJGHZ44IJXYFNOVRZGBCVKC2V62DB2KHZB7BEMYOWOLFQH4XP2TAM6B \
--value 123
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.