¿Cómo puedo inducir de manera confiable una bifurcación de blockchain para fines de prueba?

Me gustaría inducir de manera confiable una bifurcación en una red de prueba privada para poder probar el comportamiento del código fuera de la cadena de bloques que interactúa con los contratos. (ver preguntas relacionadas sobre por qué querría hacer eso). Probablemente sería útil para pruebas automatizadas de geth y otras implementaciones de nodos también.

Por confiable, me refiero a repetible, con el mismo resultado cada vez, para que pueda ejecutar esto en un sistema de prueba automatizado.

Una pregunta similar apareció en los foros de ethereum.

¿Qué sistema operativo usas? La solución podría depender de su tipo de sistema operativo.
Máquinas virtuales Linux sobre OSX, pero puedo ejecutar contenedores docker en cualquier cosa.

Respuestas (1)

La solución

  • Configure 2 instancias geth conectadas en su red privada y comience a minarlas.
  • Hay un admin.addPeer(...)comando en geth para agregar nodos pares a la lista de pares de geth, pero no parece haber ningún comando para eliminar estos pares.
  • Para simular la bifurcación , bloquee los puertos de red utilizados por ambas instancias de geth para sus conexiones punto a punto.
  • En el entorno Linux, el comando iptables se puede usar para bloquear los puertos de red. Use el firewall equivalente en Mac OSX o Windows.
  • Ambas instancias de geth desconectadas continuarían extrayendo cada una en sus copias individuales de la cadena de bloques.
  • Envíe algunas transacciones por separado en los dos nodos desconectados y gaste el doble de los montos en una dirección.
  • Vuelva a conectar las instancias de geth eliminando las reglas de bloqueo de puertos.
  • Los gastos dobles deberían desaparecer ya que la cadena de bloques de mayor dificultad se convierte en la verdadera fuente y se descarta una copia de la cadena de bloques bifurcada.
  • Este proceso debe ser repetible , aunque los tiempos de extracción de bloques cuando las transacciones enviadas se incluyen en la cadena de bloques separada (y eventualmente única) no serían los mismos de una prueba a otra .
  • (Dejaré las pruebas para otro día para verificar cómo el protocolo decide qué cadena de bloques usar cuando las instancias de geth comenzaron a comunicarse nuevamente).


Los detalles de la prueba son los siguientes:



La prueba

  • Ejecutaré dos instancias de geth en mi misma computadora
  • La primera instancia de geth usa el puerto P2P 30301 con el siguiente comando:

    user@Kumquat:~/ForkIt$ geth --datadir ./data1           \
      --genesis ~/ForkIt/etc/CustomGenesis.json             \
      --networkid 8888 --nodiscover --mine --minerthreads 1 \
      --port 30301 --maxpeers 10 console
    
  • La segunda instancia de geth usa el puerto P2P 30302 con el siguiente comando:

    user@Kumquat:~/ForkIt$ geth --datadir ./data2           \
      --genesis ~/ForkIt/etc/CustomGenesis.json             \
      --networkid 8888 --nodiscover --mine --minerthreads 1 \
      --port 30302 --maxpeers 10 console
    
  • La primera instancia de geth usa el archivo ./data1/static-nodes.jsonpara encontrar la segunda instancia de geth. Este archivo contiene la información de enodo obtenida usando el admin.nodeInfocomando de la segunda instancia de geth, donde reemplazo el texto [::] con la dirección IP de mi computadora. Así es ./data1/static-nodes.jsoncomo se ve mi:

    [
      "enode://3941d48d95d4782f8b4fb7561d78642d2e53e478e5c8d3087e6e6023f5931aca3024a9679628fff775b9ddafd11d8d48f84a502fe815619be80f16b54cb1c077@192.168.1.14:30302"
    ]
    
  • La segunda instancia de geth usa el archivo ./data2/static-nodes.jsoncon la información de enodo obtenida usando el admin.nodeInfocomando de la primera instancia de geth

  • Para simular la bifurcación, bloqueo los puertos TCP 30301 y 30302 usando los siguientes comandos:

    user@Kumquat:~/ForkIt$ sudo iptables -A INPUT -p tcp --dport 30301 -j DROP
    user@Kumquat:~/ForkIt$ sudo iptables -A INPUT -p tcp --dport 30302 -j DROP
    
  • Para permitir que las instancias geth se vuelvan a conectar como pares, elimino las reglas de bloqueo en los puertos TCP 30301 y 30302 usando los siguientes comandos:

    user@Kumquat:~/ForkIt$ sudo iptables -D INPUT -p tcp --dport 30301 -j DROP
    user@Kumquat:~/ForkIt$ sudo iptables -D INPUT -p tcp --dport 30302 -j DROP
    



La conexión y desconexión P2P

Para ver el seguimiento de la conexión P2P entre las instancias de geth, ejecute el comando admin.verbosity(6)en la consola de geth.

Cuando los puertos están desbloqueados

  • El nodo 1 muestra el siguiente mensaje:

    I0412 10:11:47.379824   12467 peer.go:173] Peer 3941d48d95d4782f 192.168.1.14:30302 broadcasted 0 message(s)
    
  • Y el nodo 2 muestra el siguiente mensaje:

    I0412 10:11:58.480153   12478 peer.go:173] Peer e0b2addf8107866c 192.168.1.14:35257 broadcasted 0 message(s)
    

Cuando se crean las reglas de iptables para bloquear los puertos P2P, las conexiones P2P entre las instancias de geth se interrumpen después de aproximadamente 1 minuto.

  • El nodo 1 muestra los siguientes mensajes:

    I0412 10:12:59.080325   12467 server.go:431] new task: static dial 3941d48d95d4782f 192.168.1.14:30302
    I0412 10:12:59.080407   12467 dial.go:209] dialing enode://3941d48d95d4782f8b4fb7561d78642d2e53e478e5c8d3087e6e6023f5931aca3024a9679628fff775b9ddafd11d8d48f84a502fe815619be80f16b54cb1c077@192.168.1.14:30302
    I0412 10:13:14.080977   12467 dial.go:212] dial error: dial tcp 192.168.1.14:30302: i/o timeout
    > admin.peers
    []
    
  • Y el nodo 2 muestra los siguientes mensajes:

    I0412 10:20:28.784346   12478 server.go:431] new task: static dial e0b2addf8107866c 192.168.1.14:30301
    I0412 10:12:58.780403   12478 dial.go:209] dialing enode://e0b2addf8107866c0e33a56f51cf800f2625ea0f4f70097ce6420b941d215c55c5404bb856d94911964865e8ac640b7ce2a2426afbf01d16d85e8f26f053a070@192.168.1.14:30301
    I0412 10:13:13.780693   12478 dial.go:212] dial error: dial tcp 192.168.1.14:30301: i/o timeout
    > admin.peers
    []
    



El tenedor

Cuando la conexión P2P está bloqueada, la cadena de bloques se bifurca con la primera instancia de geth minando en su copia separada de la cadena de bloques, y la segunda instancia de geth minando en su copia separada de la cadena de bloques.

A continuación, verá las bifurcaciones de la cadena de bloques en el bloque n.º 1405. Creé una secuencia de checkBlock()comandos que aparece en la parte inferior de esta página: la columna de la izquierda muestra el número de bloque y la columna de la derecha muestra los primeros 4 caracteres de la dirección de la base de monedas del minero.

Ejecutar el comando checkBlock(1401, 10000)en la primera instancia de geth produce el siguiente resultado:

1401    182434  0   Infinity    0   0   NaN     909f
1402    182523  89  364957.0    1   1   1.0     909f
1403    182612  178 273784.5    9   8   4.5     909f
1404    182523  89  243364.0    41  32  13.7    8d15
1405    182612  178 228176.0    51  10  12.8    8d15     <--- THE FORK
1406    182523  89  219045.4    69  18  13.8    8d15
1407    182612  178 212973.2    78  9   13.0    8d15
1408    182701  267 208648.6    90  12  12.9    8d15
1409    182612  178 205394.0    117 27  14.6    8d15
1410    182701  267 202872.6    127 10  14.1    8d15

Ejecutar el comando checkBlock(1390, 10000)en la segunda instancia de geth produce los siguientes resultados:

1401    182434  0   Infinity    0   0   NaN     909f
1402    182523  89  364957.0    1   1   1.0     909f
1403    182612  178 273784.5    9   8   4.5     909f
1404    182523  89  243364.0    41  32  13.7    8d15
1405    182612  178 228176.0    51  10  12.8    909f     <--- THE FORK
1406    182523  89  219045.4    87  36  17.4    909f
1407    182434  0   212943.5    135 48  22.5    909f
1408    182523  89  208597.7    145 10  20.7    909f
1409    182434  0   205327.2    192 47  24.0    909f
1410    182345  -89 202773.7    237 45  26.3    909f



Gasto de la misma cuenta durante la bifurcación

En ambas instancias de geth, tenía la segunda cuenta eth.accounts[1]configurada con la misma clave pública/privada.

Antes de la bifurcación, los saldos de las cuentas eran los mismos en ambas instancias de geth:

web3.fromWei(eth.getBalance(eth.accounts[1]), "ether")
10.09958

En la primera instancia de geth después de la bifurcación, transfirí 7 éteres de eth.accounts[1]a eth.accounts[0].

> eth.sendTransaction({from: eth.accounts[1], to: eth.accounts[0], value: web3.toWei(7, "ether")})
"0xe442a4e325ff6be2fbb35ba1f381e56559df722ebc307c6ad57f3580d6b97412"
...
> web3.fromWei(eth.getBalance(eth.accounts[1]), "ether")
3.09916

En la segunda instancia de geth después de la bifurcación, transfirí 6 éteres de eth.accounts[1]a eth.accounts[0].

> eth.sendTransaction({from: eth.accounts[1], to: eth.accounts[0], value: web3.toWei(6, "ether")})
"0xa06a6a141f5ab19e0be266244eb6c07c5b9bece6dc308f5fd6244dee51606fa6"
...
> web3.fromWei(eth.getBalance(eth.accounts[1]), "ether")
4.09916

Después de desbloquear el puerto P2P, ambas instancias de geth sincronizaron sus cadenas de bloques (presumiblemente con la cadena de dificultad más larga/más alta) con el resultado de que ambas instancias de geth informaron un saldo de 4.09916 en eth.accounts[1].



¿Se ajustó la dificultad hacia abajo cuando la cadena de bloques se bifurcó?

Sí, en el caso de la segunda instancia de geth, como se puede ver en el bloque #1410 a continuación:

> checkBlocks(1401,10000);
1401    182434  0       Infinity    0   0   NaN     909f
1402    182523  89      364957.0    1   1   1.0     909f
1403    182612  178     273784.5    9   8   4.5     909f
1404    182523  89      243364.0    41  32  13.7    8d15
1405    182612  178     228176.0    51  10  12.8    909f
1406    182523  89      219045.4    87  36  17.4    909f
1407    182434  0       212943.5    135 48  22.5    909f
1408    182523  89      208597.7    145 10  20.7    909f
1409    182434  0       205327.2    192 47  24.0    909f
1410    182345  -89     202773.7    237 45  26.3    909f
1411    182256  -178    200721.9    275 38  27.5    909f
1412    182168  -266    199035.2    306 31  27.8    909f
1413    182256  -178    197636.9    313 7   26.1    909f
1414    182168  -266    196447.0    358 45  27.5    909f
1415    182256  -178    195433.4    366 8   26.1    909f
1416    182168  -266    194549.0    388 22  25.9    909f
1417    182256  -178    193780.7    397 9   24.8    909f
1418    182344  -90     193107.9    407 10  23.9    909f
1419    182433  -1      192514.9    418 11  23.2    909f
1420    182344  -90     191979.6    465 47  24.5    909f
1421    182255  -179    191493.4    491 26  24.6    909f

Esperaba dificultades para ajustar hacia abajo porque la cadena de bloques combinada tenía dos mineros, mientras que la cadena de bloques bifurcada individual estaba siendo construida por un minero. Para mantener el mismo tiempo entre bloques (en promedio), la dificultad tendría que ajustarse hacia abajo.



Cosas Adicionales

~/ForkIt/etc/CustomGenesis.json

{
  "alloc": {
  },
  "nonce": "0x8888888888888888",
  "difficulty": "0x020000",
  "mixhash": "0x0000000000000000000000000000000000000000000000000000000000000000",
  "coinbase": "0x8888888888888888888888888888888888888888",
  "timestamp": "0x00",
  "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
  "extraData": "0x",
  "gasLimit": "0x888888"
}

secuencia de comandos checkBlocks()

Para el rango especificado de bloques, este script imprimirá la siguiente información: número de bloque, dificultad del bloque, cambio en la dificultad del bloque, dificultad promedio del bloque, tiempo transcurrido, cambio en el tiempo, tiempo promedio y los primeros 4 caracteres de la clave pública de la base de monedas del minero. .

function checkBlocks(firstBlock, lastBlock) {
  var i;
  var firstTimestamp;
  var prevTimestamp;
  var prevDifficulty;
  var totalDifficulty = 0;
  for (i = firstBlock; i < 10000; i++) {
    var block = eth.getBlock(i);
    if (i == firstBlock) {
      firstTimestamp = block.timestamp;
      prevTimestamp = firstTimestamp;
      prevDifficulty = block.difficulty;
    }
    if (block == null)
      break;
    totalDifficulty = +totalDifficulty + +block.difficulty;
    var averageDifficulty = totalDifficulty / (i - firstBlock);
    var averageTime = (block.timestamp - firstTimestamp) / (i - firstBlock);
    console.log(block.number + "\t" + block.difficulty + 
      "\t" + (block.difficulty - prevDifficulty) + 
      "\t" + averageDifficulty.toFixed(1) + 
      "\t" + (block.timestamp - firstTimestamp) +
      "\t" + (block.timestamp - prevTimestamp) +
      "\t" + averageTime.toFixed(1) +
      "\t" + block.miner.substr(2, 4));
    prevTimestamp = block.timestamp;
  }
}

Los resultados se parecen a los siguientes (la información de diferencia de la primera línea siempre será incorrecta):

> checkBlocks(1401,10000);
1401    182434  0   Infinity    0   0   NaN     909f
1402    182523  89  364957.0    1   1   1.0     909f
1403    182612  178 273784.5    9   8   4.5     909f
1404    182523  89  243364.0    41  32  13.7    8d15
1405    182612  178 228176.0    51  10  12.8    8d15
1406    182523  89  219045.4    69  18  13.8    8d15
1407    182612  178 212973.2    78  9   13.0    8d15
1408    182701  267 208648.6    90  12  12.9    8d15
1409    182612  178 205394.0    117 27  14.6    8d15
parece razonable, eso era algo de lo que estaba pensando. Podría intentar algo similar usando Docker. Mi jefe acaba de decidir que esto era de menor prioridad, así que no voy a marcar la respuesta hasta que funcione, dentro de unos meses...
Por cierto, el doble gasto desaparece para las interacciones en cadena como usted describe, pero no para fuera de la cadena. es decir, si como bitcoin envío 100 ether a Bob's Sporting Goods y obtengo un lindo impermeable, y luego pirateo el último nodo de minería en la ventana de 15 segundos, podría enviar esos 100 ether a Alice's Hat Shop y también obtener un lindo sombrero por el mismo éter, y Bob o Alice no tienen chaqueta ni sombrero. Son las interacciones fuera de la cadena las que son importantes. Esperar N bloques para permitir que el cambio de estado se propague es lo que les asegura a Bob y Alice que el cambio es inviolable.
Si la red se bifurca, los mineros tardarían más en resolver / extraer los bloques. El algoritmo de ajuste de dificultad debería reajustar la dificultad para reajustar el tiempo promedio de bloque. Creo que debería poder detectar una bifurcación verificando los cambios en el promedio. bloque de tiempo y/o un ajuste a la baja en dificultad. Todavía no sé cuánto tiempo / cuántos bloques se necesitarían para que el ajuste de dificultad surtiera efecto.
El navegador de cadena de bloques ya tiene detección de tío, y hay una respuesta de intercambio de pila que le permite ver si su transacción se canceló, por lo que no estoy seguro de que deba preocuparme por detectar bifurcaciones.
Está bien, creo que funciona. Muchas gracias, eso es genial. Avíseme si necesita ayuda con algo, le debo.
Lo eliminé --devde las gethinvocaciones, ya que p2p está deshabilitado en modo dev