Cómo Enviar Transacciones Gratuitas En Polkadot

Equilibrium Spanish
Sep 4 · 6 min read

Las redes blockchain dependen de las tarifas (fees) de transacción para incentivar a los nodos y mineros que hacen que toda la red funcione. Estas tarifas (fees) de transacción también sirven para proteger sus redes contra los ataques DDoS de hackers malintencionados; no es tan fácil paralizar una red con una ráfaga inmanejable de actividad de transacciones si cada transacción cuesta una cierta cantidad de dinero.

Pero hay ciertas formas de enviar transacciones en Polkadot a un costo de cero dólares. Analicemos los casos en los que una transacción de tarifa cero tiene sentido, así como cómo implementarlas realmente mientras se protege contra los ciberataques.

¿Por qué querría enviar transacciones sin comisión en Polkadot?

Hay dos casos principales en los que tiene sentido eliminar la tarifa (fee) de transacción. El primero es para incorporar nuevos usuarios, el segundo es para tomar medidas técnicas para mantener la lógica empresarial del sistema.

Cuando un nuevo usuario ya tiene tokens atribuidos y quiere reclamarlos, se enfrenta al problema de no tener una cuenta desde la que pagar las tarifas de transacción. Ya sea que esos tokens se distribuyan a través de una oferta inicial, un bloqueo o cualquier otro mecanismo relacionado, una transacción de tarifa cero resuelve el problema de entregarle a ese usuario su asignación de tokens.

Este tipo de transacciones también sirven para algunos propósitos técnicos detrás de escena, como determinar la distribución de recompensas de staking, por ejemplo. Es fundamental que un sistema DeFi se mantenga actualizado al obtener precios relevantes de los oráculos y realizar subastas de liquidación que se llevan a cabo al instante. Pero este tipo de acciones en blockchains estándar son solo transacciones ordinarias que requieren tarifas de transacción. Alguien tiene que pagar estas tarifas para respaldar el sistema, y ​​puede resultar bastante costoso mantener los precios actualizados de muchas fuentes. Tampoco es rentable pagar deudas en subastas si la ganancia es menor que el costo de transacción de la red.

Afortunadamente, las blockchains de Substrate nos permiten realizar este tipo de transacciones críticas de forma gratuita, lo que hace que el sistema sea más barato y más estable para los clientes.

Entonces, ¿cómo puedo hacer esto mientras me mantengo a salvo de los ataques DDoS?

Analicemos los tres tipos de transacciones sin fees (tarifas) que existen, y veamos la eficacia con la que previenen los ciberataques. Estas transacciones son la transacción de reembolso pagada firmada, la transacción libre firmada y las transacciones sin firmar con una carga útil firmada.

Transacciones de reembolso pagadas firmadas (Signed paid refund transactions)

Esta es probablemente la forma más sencilla de implementar transacciones gratuitas. Es mejor para aquellos casos en los que un usuario existente desea realizar una acción técnica sin pagar por ella, como usar la pallet sudo en el repositorio de Substrate.

#[pallet::weight({
let dispatch_info = call.get_dispatch_info();(dispatch_info.weight.saturating_add(10_000), dispatch_info.class)})]pub fn sudo(origin: OriginFor<T>,call: Box<<T as Config>::Call>,) -> DispatchResultWithPostInfo {// This is a public call, so we ensure that the origin is some signed account.let sender = ensure_signed(origin)?;ensure!(sender == Self::key(), Error::<T>::RequireSudo);let res = call.dispatch_bypass_filter(frame_system::RawOrigin::Root.into());Self::deposit_event(Event::Sudid(res.map(|_| ()).map_err(|e| e.error)));// Sudo user does not pay a fee.Ok(Pays::No.into())}

La idea principal aquí es que en realidad se le cobra al usuario una tarifa al comienzo de la transacción, pero esos fondos se le devuelven al final de una transacción exitosa.

Esto significa que solo un atacante potencial que envíe transacciones no válidas realmente pagará las tarifas.

Transacciones firmadas gratis (Free signed transactions)

Considere el ejemplo de obtener tokens GENS a cambio de bloquear tokens EQ en la red Equilibrium:

#[pallet::weight((10_000 + T::DbWeight::get().writes(1), Pays::No))]
pub fn claim(origin: OriginFor<T>) -> DispatchResultWithPostInfo {let who = ensure_signed(origin)?;Self::do_claim(who)?;Ok(().into())}

Este método de transacciones gratuitas es adecuado cuando la cuenta de llamada es nueva y no tiene activos para pagar las tarifas de transacción. (Esto suele ser una solicitud para recibir tokens para una nueva cuenta). Aquí la vulnerabilidad DDoS es más notable: cualquier cuenta recién generada puede saturar todo el bloque con sus transacciones sin pagar nada por ellas. Para evitar este problema, necesitamos crear una SignedExtension del siguiente tipo:

pub struct CheckAllocation<T: Config + Send + Sync>(PhantomData<T>);
impl<T: Config + Send + Sync> SignedExtension for CheckAllocation<T>whereT::Call: IsSubType<Call<T>>,{type AccountId = T::AccountId;type Call = T::Call;type AdditionalSigned = ();type Pre = ();const IDENTIFIER: &’static str = “CheckAllocation”;fn additional_signed(&self) -> sp_std::result::Result<(), TransactionValidityError> {Ok(())}fn validate(&self,who: &Self::AccountId,call: &Self::Call,_info: &DispatchInfoOf<Self::Call>,_len: usize,) -> TransactionValidity {match call.is_sub_type() {Some(Call::claim(..)) => {if !Allocations::<T>::contains_key(&who) ||ClaimStart::<T>::get().is_none() {InvalidTransaction::Call.into()} else {Ok(Default::default())}}_ => Ok(Default::default())}}}

Ahora, cuando se agrega una transacción al pool de transacciones, se verifica la validez de la transacción. En nuestro caso, comprobamos que la cuenta efectivamente tenga una asignación. El método de validación debe contener exactamente las mismas comprobaciones que se encuentran en el propio método de tiempo de ejecución. Otras llamadas deben ser predeterminadas; consulte el código anterior.

Transacciones sin firmar con una carga útil firmada (Unsigned transactions with a signed payload)

Esta es la forma más flexible de crear transacciones gratuitas, pero también es la más difícil de implementar. Así es como funciona.

Es posible que la cuenta que envía la solicitud no tenga fondos, lo cual es muy conveniente para los nodos técnicos, ya que no hay peligro de que esos fondos sean robados. Existe la posibilidad de procesar de manera óptima las acciones técnicas independientemente del remitente. Por ejemplo, no nos importa quién envió la solicitud de llamada de margen para la posición del cliente en la plataforma de préstamos de Equilibrium. Solo es importante que la solicitud sea válida y que haya exactamente una solicitud. Si usamos transacciones firmadas gratis, entonces varias llamadas de margen para un cliente enviadas por diferentes cuentas podrían terminar en el grupo de transacciones. Es posible organizar las transacciones en el orden o prioridad deseados. Por ejemplo, queremos que todas las transacciones de llamadas de margen sean las primeras del bloque.

Pero cada una de estas oportunidades conlleva algunos riesgos y vulnerabilidades potenciales.

Al igual que con las transacciones firmadas gratuitas, es importante duplicar todos los controles del tiempo de ejecución al validar una transacción. Debido a que una transacción no está firmada, la función nonces no está disponible para nosotros. Tenemos que hacer las comprobaciones para “reproducir” transacciones idénticas y crear nosotros mismos las secuencias de transacciones necesarias. Y si comete un error con la priorización de sus transacciones, entonces es posible enviar spam a todo el pool de transacciones con solo acciones técnicas, obstruyendo el resto de la funcionalidad para todos los demás.

Echemos un vistazo más profundo a nuestra implementación de oráculos para Equilibrium y Genshiro:

impl<T: Config> frame_support::unsigned::ValidateUnsigned for Pallet<T> {type Call = Call<T>;fn validate_unsigned(_source: TransactionSource, call: &Self::Call) -> TransactionValidity {if let Call::set_price_unsigned(payload, signature) = call {let signature_valid =SignedPayload::<T>::verify::<T::AuthorityId>(payload, signature.clone());if !signature_valid {return InvalidTransaction::BadProof.into();}let current_block = <frame_system::Pallet<T>>::block_number();if payload.block_number > current_block {// transaction in future?return InvalidTransaction::Stale.into();} else if payload.block_number + 5u32.into() < current_block {// transaction was in pool for 5 blocksreturn InvalidTransaction::Stale.into();}let account = payload.public.clone().into_account();Self::validate_params(account, payload.asset, payload.price).map_err(|_| InvalidTransaction::Call)?;let priority = T::UnsignedPriority::get().saturating_add(// BlockNumber is less than u64::MAX, so it is never default value on unwrappingTryInto::<u64>::try_into(payload.block_number).unwrap_or(0));ValidTransaction::with_tag_prefix("EqPrice").priority(priority).and_provides((payload.public.clone(), payload.asset)).longevity(5).propagate(true).build()} else {InvalidTransaction::Call.into()}}}

Necesitamos asegurarnos en el método de validación que permitimos transacciones sin firmar solo para un solo método, y enviamos inmediatamente un error al resto.

También debemos comprobar que la firma sea válida. Esta verificación es necesaria si la transacción solo está permitida para ciertos tipos de cuentas, como los sistemas PoS o PoA. Permitimos que las transacciones permanezcan en el pool durante no más de cinco bloques; después de eso, determinamos que el precio está desactualizado. Esto elimina los casos en los que una transacción se puede agregar al pool un número infinito de veces. (Esta verificación no es necesaria si todas las demás verificaciones se realizan correctamente, pero siempre es mejor volver a verificar).

Es necesario validar una transacción de la misma manera que se validará en tiempo de ejecución (runtime). En nuestro caso, todas las comprobaciones se mueven a un método independiente validate_params para que las comprobaciones sean 100% idénticas.

Por último, debe configurar etiquetas y prioridades. Ésta es la mayor diferencia con respecto a implementaciones anteriores. En nuestro caso, no queremos más de una transacción de una cuenta para una moneda en el pool. Esto se logra configurando and_provides con valores únicos para el pool (estos parámetros tendrán sus propios valores para cada pallet). También queremos tomar el último precio de las transacciones si hay varias transacciones dentro del pool con una propiedad and_provides idéntica. Logramos esto estableciendo la prioridad correcta. En nuestro caso, cuanto mayor sea el bloque en el que se incluyó la transacción, mayor será su prioridad.

Aquí está la conclusión: debe pensar detenidamente en la implementación correcta para cada pallet, lo que afecta el tiempo de desarrollo y aumenta las posibilidades de cometer un error. Este enfoque debe usarse solo cuando los dos primeros enfoques descritos anteriormente no sean suficientes.

Las transacciones gratuitas en Substrate son una herramienta poderosa para mantener la estabilidad del sistema e incorporar nuevos clientes, pero es muy importante elegir la forma correcta de implementarlas. De lo contrario, un pequeño error puede dejarlo vulnerable a posibles atacantes DDoS.

Equilibrium

Cross-chain Money Market