Patrones de simultaneidad para cuenta nonce

El nonce de la cuenta es el único elemento en la estructura de la transacción que evita el ataque de reproducción (como se explica en esta muy buena respuesta ). Esencialmente fuerza la secuenciación de las transacciones de una cuenta.

Esto generalmente no causa problemas cuando las transacciones se envían a través de la interfaz web3 a un nodo Ethereum (por ejemplo, Geth, Parity), que luego realiza la firma. La secuenciación la realiza internamente el nodo y, por lo tanto, es transparente para el usuario final.

Este no es el caso cuando las transacciones son creadas y firmadas por una aplicación de usuario (es decir, no Geth o Parity), antes de enviarse a un nodo (por ejemplo, a través de web3.eth.sendRawTransaction).

La construcción de la transacción sin procesar requiere obtener un nonce de cuenta, lo que se puede hacer mediante web3.eth.getTransactionCount, como se explica en esta pregunta . Esto funciona bien cuando no hay concurrencia involucrada.

Sin embargo, considere un usuario final que tiene que firmar varias transacciones en paralelo. La secuenciación de la transacción pasa a ser responsabilidad de la aplicación del usuario.

"web3.eth.getTransactionCount" solo devuelve recuentos de transacciones confirmadas (consulte esta pregunta ). Como resultado, cuando las transacciones se firman en paralelo, a menos que manipulemos el conteo de manera inteligente, solo una de ellas tendrá éxito.

Mi pregunta es: ¿cuáles son algunos buenos patrones para manejar esta situación?

Candidato 1: varias claves/cuentas por usuario

Esto permite transacciones concurrentes. Sin embargo, introduce una complejidad adicional en la aplicación del usuario. Además, no es demasiado escalable: considere si se necesitan decenas o cientos de transacciones simultáneas. Además, dado que la billetera Hierachical Deterministic no es nativa en Ethereum (soy consciente de que existe LightWallet ), esto no parece una solución fácil.

Candidato 2: bucle y reintento

Esto crea un ciclo que verifica el resultado del envío de la transacción. Si el resultado de la devolución indica un nonce duplicado (p. ej., Geth devolvería 'transacción de reemplazo con un precio bajo'), vuelva a intentarlo con un nuevo nonce.

Esta solución no es portátil: diferentes clientes devuelven diferentes cadenas para el mismo error. En la práctica, parece que he notado que Geth a veces ni siquiera devuelve la cadena anterior, sino que descarta silenciosamente las otras transacciones (no confirmado porque no he encontrado una forma confiable de reproducir). Como resultado, la detección no funciona de forma fiable.

Candidato 3: un 'administrador nonce' único

Cada construcción de transacción implica "solicitar" un nonce de un "administrador de nonce" global. El "administrador de nonce" de singleton puede tener una lógica inteligente para distribuir números de nonce ascendentes. Esto explota el comportamiento discutido aquí .

Esto no solo introduce un singleton (discutiblemente un antipatrón), sino que tampoco parece fácil hacerlo bien: si una transacción de bajo nivel falla de alguna manera (por ejemplo, ni siquiera se envía a la red), el resto de las transacciones quedará bloqueada indefinidamente. En general, es una solución pobre que introduce más complejidad y problemas de los que intenta resolver.

Candidato 4: delegado a un nodo local Geth/Parity

Esto no es realmente una solución. El problema más obvio es que la dependencia adicional no parece ser justificable, si es simplemente para el campo nonce.

Además, si la aplicación de usuario ya administra claves privadas, esto presenta una complejidad adicional: las claves privadas deberán ser coadministradas (o copiadas) por el nodo Geth o Parity. Nuevamente, la complejidad (y la vulnerabilidad potencial) no parece justificable si es solo para tratar con el nonce.


Sin relación. Me parece que la cuenta nonce no es el mejor diseño en Ethereum. Obliga a las aplicaciones de usuario a manejar detalles de protocolo de bajo nivel o a introducir dependencia en un nodo (Geth/Parity). De cualquier manera, agrega un volumen desproporcionado a las aplicaciones de los usuarios.

Respuestas (3)

Todavía no he llegado al punto en el que pueda probar ninguna de estas cosas, pero mi intuición es que Singleton Nonce Manager sería el camino a seguir, con algunas mejoras:

  • Conviértalo en un remitente de transacciones singleton
  • Que tiene un búfer máximo o 'cabeza' de transacciones pendientes por dirección 'de' que coincide o es menor que el número máximo de transacciones que el par puede tener en su cola de grupo de transacciones por dirección de. Llamemos a estas transacciones pendientes 'slots'.
  • Se implementa como una cola continua, donde las transacciones confirmadas más antiguas se eliminan de la 'cola'.
  • Cada nueva ranura obtiene un nuevo nonce
  • Cada nueva ranura está asociada con una llamada asíncrona a sendRawTransaction. En caso de falla de cualquier tipo, la ranura queda libre.
  • Cualquier transacción nueva se agrega al espacio libre más bajo.
  • En el caso de que las ranuras más altas estén pendientes durante más de N milisegundos después de que se libere una ranura por error, cancele la transacción más alta enviándose 0 eth a sí mismo con el mismo nonce que la ranura más alta y reproduciendo esa transacción en la ranura liberada.

Los detalles de implementación requerirían una familiaridad íntima con la programación concurrente, bloqueos, mutexs, asíncrono o lo que sea. Esencialmente, lo anterior es una suposición, ya que aún no he trabajado directamente con las llamadas RPC y no puedo probarlo, pero creo que esa es la base de la ruta que seguiría. Veo que la estructura de la cola ofrecería una API interna para enumerar las transacciones pendientes, procesadas y admitir otras operaciones de la interfaz de usuario como bonificación.

Buen diseño. Muchas gracias. El búfer de anillo parece una solución brillante.
'Búfer circular'. Sí. Esa es una frase mucho mejor que 'cola rodante'. 👍
Después de días de pensar que me estoy volviendo loco y mi programación simplemente apesta (y lo hace), este es el mismo problema al que me enfrento: potencialmente muchas transacciones firmadas por múltiples aplicaciones al mismo tiempo con administración nonce. Ethers.js, una versión "mejor" de Web3, tiene un NonceManager próximo, pero el contador de nonce aún depende de la red para obtener el recuento de transacciones, por lo que no estoy muy seguro de cómo se pretende que eso resuelva el problema. ¿Tú, @LinmaoSong, has descubierto algo en los últimos dos años?
@EvilJordan lo siento, lamentablemente no. Es un patrón singleton a nivel de protocolo, por lo que un administrador singleton es el menos malo
¿Por qué la cantidad de ranuras en el búfer nonce tiene que ser " menor que la cantidad máxima de transacciones que el par puede tener en su cola de grupo de transacciones por dirección de origen "? Me parece que no necesitamos esta limitación siempre que no intentemos publicar todas las transacciones al mismo tiempo.

getTransactionCount también puede incluir transacciones pendientes, que es una mejor manera de determinar el nonce para una transacción sin procesar posterior. Esta solución es simple pero poco probable sin sus propios problemas. Pero para disparar un puñado de transacciones en estrecha sucesión, parece funcionar como se esperaba.

nonce: web3.toHex(web3.eth.getTransactionCount(fromAddress, "pending")),
Gracias Ryanh. Esto solo funciona si una transacción se crea solo después de que se hayan enviado otras transacciones Y ya se hayan aceptado en el grupo de transacciones. Considere dos transacciones que se construyen al mismo tiempo, incluso con el indicador 'pendiente', obtendrán el mismo resultado. Así que el problema todavía existe.
@LinmaoSong He tenido el retorno de getTransactionCount() aún sin cambios (es decir, no incrementado) incluso después de haber recibido el hash Tx, y no vi que se incrementara hasta que se extrajo la transacción. Tenga en cuenta que esto estaba pasando por Web3.js si eso importa. Estaba obteniendo nonce duplicados debido a esto.

Todo depende de tu contexto. Si es un usuario final que intenta emitir múltiples transacciones usted mismo, simplemente puede almacenar el nonce en algún lugar e incrementarlo en cada transacción paralela. Obviamente, desea evitar la creación de brechas en el momento, pero siempre que esté 100% seguro de que puede cubrir los fondos cuando se ejecutan las transacciones, una brecha está bien temporalmente. Lo peor que puede pasar es que un nonce se reproduzca dos veces, en cuyo caso, ha creado transacciones no válidas.

Si actúa en nombre de un usuario e intenta emitir transacciones masivas, podría usar una operación de base de datos atómica para almacenar el nonce. Esto podría funcionar en una billetera de procesamiento por lotes hipotética que intente enviar varias transacciones a la vez. Probablemente desee incluir un descargo de responsabilidad sobre el hecho de que un usuario debe evitar usar la clave mientras interactúa con la billetera, para evitar que el contador interno se desincronice.

Gracias. Me parece que almacenar el nonce (en DB o donde sea) es esencialmente el patrón singleton que mencioné anteriormente. ¿Cómo lidiaría con el problema de falla de transacción de menor nonce?
Quiero decir, definitivamente seguiría la ruta de asegurarme de que se realice una transmisión, a través de otros medios. Por ejemplo, establezca dinámicamente el precio de cada gas por encima del precio medio del gas para asegurarse de que la red realmente lo procese. Tal vez aumente un poco más el precio de la gasolina para garantizar que siempre esté "a la vanguardia". Siempre puede ver una transacción o un conjunto de transacciones, y si parece que falta una, simplemente retransmita la que falta. es mucho trabajo de contabilidad, pero potencialmente vale la pena.