Bitcoin micropayments: descifrando el futuro de las pagos pequeños

Valoración: 3.51 (632 votos)

Bitcoin ha emergido como una plataforma con un potencial enorme para habilitar los micropagos, pagos mucho más pequeños de lo que el sistema financiero tradicional puede manejar. De hecho, puedes enviar una cantidad muy pequeña de valor en una transacción de Bitcoin sin hacer nada especial y funcionará, incluso si lo que estás enviando es solo una fracción de un centavo. Sin embargo, hacerlo está sujeto a algunas advertencias importantes.

Si envías demasiadas transacciones demasiado rápido, serán relegadas o no serán retransmitidas por varios algoritmos anti-inundación integrados en la red Bitcoin. Existe una cantidad mínima de valor que una sola transacción puede enviar, determinada por el número de bytes necesarios para enviar y reclamarla junto con las tarifas cobradas. El receptor de los micropagos termina con una billetera llena de "polvo" que puede ser costoso de gastar en términos de tarifas. Esto es un equivalente aproximado del mismo problema que tendrías con las monedas de metal, si cobraras a mucha gente un centavo, repetidamente. Terminarías con una bolsa llena de pequeñas monedas de metal que la mayoría de los comerciantes se negarían a aceptar como pago, porque los gastos generales de la transacción serían mayores que el valor del dinero.

A veces, estas restricciones no importan: solo deseas realizar un solo pago muy pequeño y el receptor no recibirá muchos de ellos de todos modos. Pero otras veces, estas restricciones son demasiado limitantes y debemos buscar un enfoque alternativo. Este artículo describe cómo usar los canales de pago, una forma de configurar una transferencia de valor pendiente de una billetera a otra, de modo que la cantidad que se transferirá sea incrementable a alta velocidad y en cantidades muy pequeñas. Si bien esto no te permite enviar micropagos a alta velocidad a diferentes destinatarios cada vez, muchas aplicaciones pueden encajar en este marco, generalmente cualquier cosa que implique una microfacturación por un servicio medido.

Tabla de Contenido

Canales de Pago : La Solución para Micropagos Eficientes

A partir de bitcoinj 0.10, puedes usar canales de pago para implementar varios tipos de aplicaciones de facturación por uso. Proporcionamos una subbiblioteca que implementa un protocolo cliente/servidor para esto, junto con un ejemplo de aplicación cliente/servidor que muestra lo fácil que es usarla. Debido a que la tecnología de los canales de pago todavía es nueva y experimental, si deseas usarla, ponte en contacto con nosotros primero y háznos saber lo que estás haciendo, para asegurarnos de que te mantengas actualizado a medida que el código evoluciona.

Descripción General del Protocolo

El protocolo de micropago permite que una parte (el cliente) realice micropagos repetidos a otra parte (el servidor). Funciona en dos etapas. En primer lugar, se bloquea algo de valor con una transacción de firma múltiple que lo coloca bajo el control de ambas partes. Las partes colaboran para crear una transacción de reembolso firmada que gasta todo el valor de vuelta al cliente, pero está bloqueada en el tiempo utilizando la función nLockTime del protocolo Bitcoin. Esto asegura que el reembolso no se volverá válido hasta que haya transcurrido un período de tiempo (actualmente, un día).

La transacción de reembolso se prepara de tal manera que el cliente obtiene una copia totalmente firmada antes de que se envíe la transacción de firma múltiple inicial (el contrato) al servidor. De esta manera, evitamos un posible bloqueo/ataque que podría hacer que el cliente pierda dinero: una vez que el cliente recibe la transacción de reembolso, solo entonces se bloquea el dinero para que ambas partes lo controlen. Si el servidor se detiene en algún momento del protocolo, el cliente siempre puede recuperar su dinero.

Una vez que el cliente ha obtenido la transacción de reembolso, transmite el contrato de firma múltiple al servidor, que luego lo firma y lo transmite, bloqueando así el dinero y abriendo el canal. Para realizar un pago, el cliente prepara y firma una nueva copia de la transacción de reembolso que reembolsa ligeramente menos dinero que antes. La firma se envía al servidor, que luego verifica que la firma es correcta y la almacena. La firma utiliza modos SIGHASH bastante flexibles, por lo que el servidor tiene mucha libertad para modificar la transacción de reembolso como lo desee, pero normalmente solo agregará una salida que se envía de vuelta a su propia billetera.

De esta manera, una vez que se establece el canal de pago, se puede realizar un micropago con solo una operación de firma por parte del cliente y una verificación por parte del servidor.

Eventualmente, el cliente decidirá que ha terminado. En este punto, envía un mensaje al servidor pidiéndole que cierre el canal. El servidor luego firma la versión final del contrato con su propia clave y la transmite, lo que hace que el estado final del canal se confirme en la cadena de bloques. Si el servidor no coopera o ha desaparecido antes de que el cliente tenga la oportunidad de cerrar el canal limpiamente, entonces el cliente debe esperar 24 horas hasta que la transacción de reembolso inicial se vuelva válida.

Como puedes ver, debido a que la transacción de reembolso permite al cliente recuperar todo su dinero si el canal todavía está en uso a medida que se acerca el tiempo de caducidad, el servidor debe cerrar el canal y se debe construir uno nuevo desde cero.

Diseño de la API

Bitcoinj proporciona una serie de objetos que implementan las partes de cliente y servidor de la disposición anterior. El protocolo de cableado utiliza TCP para enviar búferes de protocolo con prefijo de longitud uint, pero el sistema está diseñado para ser incrustable, por lo que puedes vincular fácilmente micropagos en otros protocolos, como HTTP o XMPP.

En el corazón de la API hay dos objetos de estado, PaymentChannelClientState y PaymentChannelServerState. Estas clases operan independientemente de cualquier protocolo de red en particular y proporcionan una máquina de estado en la que eres responsable de proporcionar y recuperar los objetos correctos en los momentos adecuados. Por ejemplo, al llamar al método incrementPaymentBy, lo que obtienes es una matriz de bytes que contiene una firma, pero los objetos de estado no realizarán ninguna transmisión de esos datos por ti. Normalmente, sin embargo, deseas serializar estas transiciones de máquina de estado en matrices de bytes, listas para la transmisión de red. Además, deseas persistir esa máquina de estado en el disco para que puedas reanudar los micropagos a través de reinicios de la aplicación o interrupciones de la conectividad de red. Esto está implementado por PaymentChannelClient y PaymentChannelServer. Estos objetos toman los parámetros básicos del canal y un objeto que implementa una interfaz específica de cliente/servidor. Construyen las máquinas de estado, serializan las transiciones a mensajes basados ​​en búferes de protocolo y utilizan una extensión de billetera (ver tercer punto) para garantizar que los datos del canal se almacenen dentro del archivo de la billetera. También realizan una verificación de errores del protocolo para garantizar que un cliente o servidor malicioso no pueda hacer nada extraño. El protocolo requiere que se tomen ciertas acciones en ciertos momentos, donde el tiempo está definido por los campos de tiempo en la cadena de bloques. Los StoredPaymentChannel{Client/Server}States actúan como extensiones de billetera y observan la cadena de bloques para tomar las acciones correctas en los momentos correctos. Eso significa que si conduces las clases de máquina de estado directamente, las transacciones de reembolso no se transmitirán en el momento adecuado para ti y tendrás que implementar esa lógica tú mismo. Aunque obtener/proporcionar búferes de protocolo es un gran paso adelante con respecto a los objetos Java sin procesar, todavía no es suficiente, ya que tenemos que manejar la lectura y escritura de ellos en la red. A menudo, el protocolo de micropago se incrustará dentro de otro protocolo. Pero si deseas que se ejecute de forma independiente (por ejemplo, para probar), proporcionamos las clases PaymentChannelClientConnection y PaymentChannelServerListener. Toman pares de host/puerto y los parámetros del canal, luego construyen y unen el resto de los objetos. Delegan el código de manejo real de la red a un paquete separado (org.bitcoinj.protocols.niowrapper) que simplemente lee/escribe búferes de protocolo con prefijo de longitud a través de una conexión TCP.

Aunque esto puede parecer una gran cantidad de objetos, las abstracciones tienen un propósito. Imagina construir un protocolo que te permita pagar por no ver anuncios en la web haciendo micropagos privados a las redes publicitarias en el momento en que se va a mostrar el anuncio. Una conexión TCP separada probablemente no sea la herramienta adecuada para usar aquí. En cambio, tendría más sentido extender HTTP con algunos encabezados especiales y vincular el navegador a tu aplicación de billetera, para que el protocolo de micropagos fluya sobre esos encabezados HTTP en línea. En ese caso, querrías usar la máquina de estado y posiblemente la serialización de protobuf, pero el código de red en sí mismo podría no ser tan útil. Yendo aún más lejos, si estás incrustando el protocolo en algo que ya tiene su propio mecanismo de serialización, es posible que desees reutilizar las máquinas de estado centrales pero evitar por completo los búferes de protocolo. Todos estos casos de uso son posibles.

Tutorial: Implementando un Servidor y un Cliente de Micropagos

Servidor

Veamos el servidor de juguete incluido en el paquete de ejemplos.

public void run () throws Exception { NetworkParameters params = TestNet3Params . get (); // Crea todos los objetos que necesitamos, crea/carga una billetera, sincroniza la cadena, etc. Anulamos WalletAppKit para poder // personalizarlo agregando los objetos de extensión: tenemos que hacer esto antes de que se cargue el archivo de la billetera, por lo que // el complemento que sabe cómo analizar todos los datos adicionales está presente durante la carga. appKit = new WalletAppKit ( params , new File ( "." ), "payment_channel_example_server" ) { @Override protected void addWalletExtensions () { // Los StoredPaymentChannelServerStates persisten los canales para que podamos reanudarlos // después de un reinicio. storedStates = new StoredPaymentChannelServerStates ( wallet (), peerGroup ()); wallet (). addExtension ( storedStates ); } }; appKit . startAndWait (); // Proporcionamos un grupo de pares, una billetera, un tiempo de espera en segundos, la cantidad que necesitamos para iniciar un canal y // una implementación de HandlerFactory, que simplemente implementamos nosotros mismos. new PaymentChannelServerListener ( appKit . peerGroup (), appKit . wallet (), 15 , Utils . COIN , this ). bindAndStart ( 4242 ); }

Aquí está el núcleo de una aplicación bitcoinj normal. Seleccionamos nuestros parámetros de red, la red de prueba en este caso, y luego construimos un WalletAppKit que nos da todo lo que necesitamos para hacer negocios con Bitcoin. Lo único inusual aquí es que subclaseamos el kit de aplicaciones y anulamos uno de sus métodos, addWalletExtensions.

Las extensiones de billetera son un mecanismo de complemento que te permite persistir datos arbitrarios dentro de un archivo de billetera bitcoinj (que es básicamente un búfer de protocolo grande). Son objetos Java que implementan una interfaz específica y el código de canales de pago proporciona una extensión para que el estado del canal se pueda guardar automáticamente. Sin embargo, el objeto de extensión debe agregarse al objeto de billetera antes de que se cargue desde el disco, para garantizar que los datos de la extensión guardados se deserialicen correctamente. Hacemos eso aquí usando un gancho que WalletAppKit nos proporciona. Todas las aplicaciones que usan canales de micropago necesitan hacer esto.

Tener en cuenta que la extensión de la billetera toma un PeerGroup como argumento. La razón es que a medida que un canal se acerca a su tiempo de caducidad, el servidor sabe que debe cerrarlo y transmitir el estado final antes de que el cliente tenga la oportunidad de usar su transacción de reembolso. Si el servidor solo se ejecuta de forma intermitente, es posible que pierda todo el dinero que se acumuló hasta ahora, por lo que si tu servidor es transitorio, ¡asegúrate de que el sistema operativo lo despierte en los momentos adecuados!

Una vez que hemos establecido nuestras conexiones a la red Bitcoin y hemos sincronizado la cadena, enlazamos e iniciamos el objeto servidor. Damos un tiempo de espera que se utiliza para las comunicaciones de red (esto es distinto de la duración máxima del canal, que actualmente está codificada). Se le da "this" como parámetro: la razón es que la clase PaymentChannelServerListener nos llamará de vuelta cuando se realice una nueva conexión entrante. Se espera que regresemos de esa devolución de llamada global un objeto que recibirá devoluciones de llamada para esa conexión específica. Así que hagamos eso.

@Override public ServerConnectionEventHandler onNewConnection ( final SocketAddress clientAddress ) { // Cada conexión necesita un controlador que se informa cuando se ajusta ese canal de pago. Aquí solo registramos // cosas. En una aplicación real, este objeto estaría conectado a alguna lógica empresarial. return new ServerConnectionEventHandler () { @Override public void channelOpen ( Sha256Hash channelId ) { log . info ( "Channel open for {}: {}." , clientAddress , channelId ); // Intenta obtener el objeto de estado del conjunto de estados almacenados en nuestra billetera PaymentChannelServerState state = null ; try { state = storedStates . getChannel ( channelId ). getState ( appKit . wallet (), appKit . peerGroup ()); } catch ( VerificationException e ) { // Esto indica datos corruptos, y dado que el canal acaba de abrirse, no puede suceder throw new RuntimeException ( e ); } log . info ( " with a maximum value of {}, expiring at UNIX timestamp {}." , // El valor máximo del canal es el valor del contrato multisig que bloquea una // cantidad de dinero en el estado del canal . getMultisigContract (). getOutput ( 0 ). getValue (), // El canal expira a un cierto desplazamiento desde que la transacción de reembolso del cliente se vuelve // gastable. state . getRefundTransactionUnlockTime () + StoredPaymentChannelServerStates . CHANNEL_EXPIRE_OFFSET ); } @Override public void paymentIncrease ( Coin by , Coin to ) { log . info ( "Client {} paid increased payment by {} for a total of " + to . toString (), clientAddress , by ); } @Override public void channelClosed ( PaymentChannelCloseException . CloseReason reason ) { log . info ( "Client {} closed channel for reason {}" , clientAddress , reason ); } }; }

La interfaz es simple: se nos informa cuando un canal se abre correctamente, junto con un "ID de canal" que lo identifica de una manera independiente de la capa de red. Una vez que tenemos el channelId, podemos consultar la extensión de la billetera que creamos anteriormente para obtener el objeto de estado canónico, del cual podemos obtener información más detallada sobre el canal. Para la mayoría de los casos de uso, esto probablemente no sea necesario, ya que la extensión de la billetera se encarga de la expiración del canal por ti y el valor máximo del canal no es una estadística particularmente útil (ya se especifica un mínimo en el constructor del escucha del servidor, al que la mayoría de los clientes se ajustarán de forma predeterminada).

Después de la devolución de llamada channelOpen, se nos dice cuándo recibimos un nuevo pago y, finalmente, se nos dice cuándo se cierra el canal y por qué.

Cliente

Del lado del cliente, la primera parte se ve muy similar, excepto que en la billetera agregamos un StoredPaymentChannelClientStates (observe Client en lugar de Server).

Luego, elegimos algunos parámetros del canal y luego intentamos construir un objeto PaymentChannelClientConnection. Esto podría reanudar un canal de pago anterior si tenemos uno disponible con el mismo ID de canal. El ID del canal es solo una cadena opaca que se envía como un hash al servidor. En este caso, lo establecemos como el nombre de host, por lo que hablar con el mismo servidor siempre utilizará el mismo canal, incluso si ambos lados se reinician o cambia su dirección IP.

Luego iniciamos un bucle donde intentamos construir el canal, pero si aún no tenemos suficiente dinero en nuestra billetera, esperamos hasta que lo tengamos y luego volvemos a intentarlo.

// Crea el objeto que administra el protocolo de canales de pago, lado del cliente. Dígale dónde está el servidor a // conectar, junto con algunos tiempos de espera de red razonables, la billetera y nuestra clave temporal. También tenemos // que elegir una cantidad de valor para bloquear durante la duración del canal. // // Tenga en cuenta que esto puede o no construir realmente un nuevo canal. Si se encuentra un canal existente no cerrado en // la billetera, entonces volverá a utilizar ese en su lugar. final int timeoutSecs = 15 ; final InetSocketAddress server = new InetSocketAddress ( host , 4242 ); PaymentChannelClientConnection client = null ; while ( client == null ) { try { final String channelID = host ; client = new PaymentChannelClientConnection ( server , timeoutSecs , appKit . wallet (), myKey , maxAcceptableRequestedAmount , channelID ); } catch ( ValueOutOfRangeException e ) { // Todavía no tenemos suficiente dinero en nuestra billetera. Espere e inténtelo de nuevo. waitForSufficientBalance ( maxAcceptableRequestedAmount ); } }

El método waitForSufficientBalance es simple y no específico de los micropagos, pero lo incluimos aquí por completitud:

private void waitForSufficientBalance ( Coin amount ) { // No hay suficiente dinero en la billetera. Coin amountPlusFee = amount . add ( SendRequest . DEFAULT_FEE_PER_KB ); ListenableFuture < Coin > balanceFuture = appKit . wallet (). getBalanceFuture ( amountPlusFee , Wallet . BalanceType . AVAILABLE ); if (! balanceFuture . isDone ()) { System . out . println ( "Please send " + amountPlusFee . toFriendlyString () + " BTC to " + myKey . toAddress ( params )); Futures . getUnchecked ( balanceFuture ); // Espere. } }

Una vez que tenemos un PaymentChannelClientConnection construido correctamente, esperamos a que se abra de nuevo (o se reanude):

// Abrir el canal requiere hablar con el servidor, por lo que es asíncrono. Futures . addCallback ( client . getChannelOpenFuture (), new FutureCallback < PaymentChannelClientConnection >() { @Override public void onSuccess ( PaymentChannelClientConnection client ) { .... } @Override public void onFailure ( Throwable throwable ) { .... } }

Debido a que implica algo de charla de red, este proceso es asíncrono y obtenemos un futuro que nos permite saber cuándo se completó o falló. Por supuesto, podemos encadenar estos futuros juntos con otros y realizar todas las operaciones habituales en ellos.

En nuestro método onSuccess tenemos esto:

// ¡Éxito! Deberíamos poder intentar realizar micropagos ahora. Intente hacerlo 10 veces. for ( int i = 0 ; i < 10 ; i ++) { try { client . incrementPayment ( Utils . CENT ); } catch ( ValueOutOfRangeException e ) { log . error ( "Failed to increment payment by a CENT, remaining value is {}" , client . state (). getValueRefunded ()); System . exit (- 3 ); } log . info ( "Successfully sent payment of one CENT, total remaining on channel is now {}" , client . state (). getValueRefunded ()); Uninterruptibles . sleepUninterruptibly ( 500 , MILLISECONDS ); } // Ahora dile al servidor que hemos terminado para que transmitan la transacción final y nos reembolsen lo que queda. Si nunca hacemos esto, entonces eventualmente el servidor se agotará y lo hará de todos modos, y si // el servidor desaparece por más tiempo, entonces eventualmente NOSOTROS se agotará y la transacción de reembolso se transmitirá // por nosotros mismos. log . info ( "Closing channel!" ); client . close ();

Así que enviamos 1 bitcent cada medio segundo, y luego cuando terminamos, cerramos el canal.

Elegir los Parámetros del Canal

Para construir un canal de pago, debes elegir algunos parámetros, en particular, cuánto dinero debes bloquear. Ten en cuenta que en el lado del cliente especificas un máximo, y luego el servidor solicita la cantidad real que está dispuesto a aceptar en un solo canal, por lo que no hay garantía de que la cantidad total que especifica el lado del cliente termine en un contrato de firma múltiple. No hay una regla estricta sobre qué elegir aquí, depende de lo que los usuarios de tu aplicación estén dispuestos a tolerar.

Ten en cuenta que el período de expiración para los canales actualmente no es configurable: siempre es un día.

Alternativas

También existe una idea llamada Lightning Network, basada en canales de pago bidireccionales más modernos.

El Futuro de los Micropagos

Los micropagos en Bitcoin tienen un potencial enorme para revolucionar la forma en que interactuamos con los servicios digitales. Desde pagos por contenido, hasta sistemas de microfacturación y modelos de suscripción, esta tecnología está allanando el camino para un ecosistema digital más descentralizado y eficiente. A medida que la tecnología de canales de pago continúa evolucionando, podemos esperar ver aún más casos de uso innovadores que aprovechen la potencia de los micropagos en Bitcoin.

Si quieres conocer otros artículos parecidos a Bitcoin micropayments: descifrando el futuro de las pagos pequeños puedes visitar la categoría Finanzas / Inversiones.

Subir