Consultando las pruebas de Blobstream

Requisitos previos

  • Acceso a una Celestia nodo completo de consenso Punto final de RPC (o nodo completo). El nodo no necesita ser un nodo de validación para que se consulten las pruebas. Un nodo completo es suficiente.

Descripción general de las consultas de prueba

Para probar la inclusión de transacciones, blobs o acciones de PayForBlobs (PFB), comprometidas en un bloque de Celestia, utilizamos el RPC del nodo de consenso de Celestia para buscar pruebas que puedan verificarse en un contrato de liquidación nominal a través de Blobstream. De hecho, cuando una transacción PFB se incluye en un bloque, se separa en una transacción PFB (sin el blob) y el blob de datos real que lleva. Estos dos se dividen en acciones, que son las construcciones de bajo nivel de un bloque Celestia, y se guardan en el bloque Celestia correspondiente. Obtenga más información sobre las acciones en el especificaciones de acciones.

Los dos diagramas a continuación resumen cómo se compromete una sola acción, que puede contener una transacción PFB, o una parte de los datos de despliegue que se publicaron utilizando un PFB, en Blobstream.

La parte se destaca en verde. R0, R1 etc., representan las respectivas raíces de fila y columna, los gradientes azul y rosa son datos codificados de borrado. Se pueden encontrar más detalles sobre el diseño cuadrado en el diseño del cuadrado de datos y estructuras de datos parte de las especificaciones.

La plaza Celestia

El esquema de compromiso

Entonces, para demostrar la inclusión de una acción en un bloque de Celestia, usamos Blobstream como fuente de verdad. Actualmente, utilizaremos la implementación de Blobstream X de Blobstream, se puede encontrar más información sobre Blobstream X en la visión general. En pocas palabras, Blobstream X atestigua los datos publicados en Celestia en el contrato de Blobstream X mediante la verificación de una prueba zk de los encabezados de un lote de bloques de Celestia. Luego, mantiene referencia de ese lote de bloques utilizando el compromiso merkleizado de sus (dataRoot, height) resultando en a data root tuple root. Compruebe el diagrama anterior que muestra:

  • 0: esas son las acciones, que cuando están unificadas, contienen el PFB o el blob de datos de rollup.

  • 1: las raíces de fila y columna son las raíces del árbol merkle del espacio de nombres sobre las acciones. Más información sobre el NMT en el especificaciones NMT. Estos se comprometen con las filas y columnas que contienen las acciones anteriores.

  • 2: las raíces de datos: que son el compromiso binario del árbol de merkle sobre las raíces de fila y columna. Esto significa que si puede probar que una acción es parte de una fila, use una prueba de merkle de espacio de nombres. Luego pruebe que esta fila está comprometida con la raíz de datos. Entonces puede estar seguro de que esa acción se publicó en el bloque correspondiente.

  • 3: para agrupar varios bloques en el mismo compromiso, creamos un compromiso sobre el (dataRoot, height) tupla para un lote de bloques, lo que da como resultado una raíz de tupla de raíz de datos. Es este compromiso el que se almacena en el contrato inteligente de Blobstream X.

Entonces, si podemos demostrar que una acción es parte de una fila, entonces esa fila está comprometida por una raíz de datos. Luego, demuestre que esa raíz de datos junto con su altura está comprometida con la raíz de tupla de raíz de datos, que se guarda en el contrato de Blobstream X, podemos estar seguros de que esa participación se comprometió en el bloque Celestia correspondiente.

En este documento, proporcionaremos detalles sobre cómo consultar las pruebas anteriores y cómo adaptarlas para enviarlas a un contrato de rollup para su verificación.

Demostración práctica

Esta parte proporcionará los detalles de la generación de pruebas y la forma de hacer que los resultados de las consultas de pruebas estén listos para ser consumidos por el contrato de implementación objetivo.

NOTA

Para los fragmentos de cliente de go, asegúrese de tener los siguientes reemplazos en su go.mod:

ir

// go.mod
    github.com/cosmos/cosmos-sdk => github.com/celestiaorg/cosmos-sdk v1.18.3-sdk-v0.46.14
    github.com/gogo/protobuf => github.com/regen-network/protobuf v1.3.3-alpha.regen.1
    github.com/syndtr/goleveldb => github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7
    github.com/tendermint/tendermint => github.com/celestiaorg/celestia-core v1.32.0-tm-v0.34.29

)

Además, asegúrese de actualizar las versiones para que coincidan con las últimas github.com/celestiaorg/cosmos-sdk y github.com/celestiaorg/celestia-core versiones.

1. Prueba de inclusión de raíz de datos

Para demostrar que la raíz de datos está comprometida con el contrato inteligente Blobstream X, tendremos que proporcionar una prueba Merkle de la tupla raíz de datos a una raíz de tupla raíz de datos. Esto se puede crear usando el data_root_inclusion_proof consulta.

Esto punto final permite consultar una raíz de datos a prueba de raíz de tupla de raíz de datos. Se necesita un bloque height, un bloque de inicio y un bloque final, luego genera la prueba binaria Merkle del DataRootTuple, correspondiente a eso height, a la DataRootTupleRoot que está comprometido en el contrato de Blobstream X.

El punto final se puede consultar utilizando el cliente golang:

ir

package main

import (
	"context"
	"fmt"
	"github.com/tendermint/tendermint/rpc/client/http"
	"os"
)

func main() {
	ctx := context.Background()
	trpc, err := http.New("tcp://localhost:26657", "/websocket")
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
	err = trpc.Start()
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
	dcProof, err := trpc.DataRootInclusionProof(ctx, 15, 10, 20)
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
	fmt.Println(dcProof.Proof.String())
}

Ejemplo completo de prueba de que un bloque Celestia fue comprometido por el contrato Blobstream X

ir

package main

import (
	"context"
	"fmt"
	"github.com/celestiaorg/celestia-app/pkg/square"
	"github.com/ethereum/go-ethereum/accounts/abi/bind"
	ethcmn "github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/ethclient"
	blobstreamxwrapper "github.com/succinctlabs/blobstreamx/bindings"
	"github.com/tendermint/tendermint/crypto/merkle"
	"github.com/tendermint/tendermint/rpc/client/http"
	"math/big"
	"os"
)

func main() {
	err := verify()
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
}

func verify() error {
	ctx := context.Background()

	// start the tendermint RPC client
	trpc, err := http.New("tcp://localhost:26657", "/websocket")
	if err != nil {
		return err
	}
	err = trpc.Start()
	if err != nil {
		return err
	}

	// get the PayForBlob transaction that contains the published blob
	tx, err := trpc.Tx(ctx, []byte("tx_hash"), true)
	if err != nil {
		return err
	}

	// get the block containing the PayForBlob transaction
	blockRes, err := trpc.Block(ctx, &tx.Height)
	if err != nil {
		return err
	}

	// get the nonce corresponding to the block height that contains
	// the PayForBlob transaction
	// since BlobstreamX emits events when new batches are submitted,
	// we will query the events
	// and look for the range committing to the blob
	// first, connect to an EVM RPC endpoint
	ethClient, err := ethclient.Dial("evm_rpc_endpoint")
	if err != nil {
		return err
	}
	defer ethClient.Close()

	// use the BlobstreamX contract binding
	wrapper, err := blobstreamxwrapper.NewBlobstreamX(ethcmn.HexToAddress("contract_Address"), ethClient)
	if err != nil {
		return err
	}

	LatestBlockNumber, err := ethClient.BlockNumber(context.Background())
	if err != nil {
		return err
	}

	eventsIterator, err := wrapper.FilterDataCommitmentStored(
		&bind.FilterOpts{
			Context: ctx,
			Start: LatestBlockNumber - 90000,
			End: &LatestBlockNumber,
		},
		nil,
		nil,
		nil,
	)
	if err != nil {
		return err
	}

	var event *blobstreamxwrapper.BlobstreamXDataCommitmentStored
	for eventsIterator.Next() {
		e := eventsIterator.Event
		if int64(e.StartBlock) <= tx.Height && tx.Height < int64(e.EndBlock) {
			event = &blobstreamxwrapper.BlobstreamXDataCommitmentStored{
				ProofNonce:     e.ProofNonce,
				StartBlock:     e.StartBlock,
				EndBlock:       e.EndBlock,
				DataCommitment: e.DataCommitment,
			}
			break
		}
	}
	if err := eventsIterator.Error(); err != nil {
		return err
	}
	err = eventsIterator.Close()
	if err != nil {
		return err
	}
	if event == nil {
		return fmt.Errorf("couldn't find range containing the transaction height")
	}

	// get the block data root inclusion proof to the data root tuple root
	dcProof, err := trpc.DataRootInclusionProof(ctx, uint64(tx.Height), event.StartBlock, event.EndBlock)
	if err != nil {
		return err
	}

	// verify that the data root was committed to by the BlobstreamX contract
	committed, err := VerifyDataRootInclusion(ctx, wrapper, event.ProofNonce.Uint64(), uint64(tx.Height), blockRes.Block.DataHash, dcProof.Proof)
	if err != nil {
		return err
	}
	if committed {
		fmt.Println("data root was committed to by the BlobstreamX contract")
	} else {
		fmt.Println("data root was not committed to by the BlobstreamX contract")
		return nil
	}
    return nil
}

func VerifyDataRootInclusion(
	_ context.Context,
	blobstreamXwrapper *blobstreamxwrapper.BlobstreamX,
	nonce uint64,
	height uint64,
	dataRoot []byte,
	proof merkle.Proof,
) (bool, error) {
	tuple := blobstreamxwrapper.DataRootTuple{
		Height:   big.NewInt(int64(height)),
		DataRoot: *(*[32]byte)(dataRoot),
	}

	sideNodes := make([][32]byte, len(proof.Aunts))
	for i, aunt := range proof.Aunts {
		sideNodes[i] = *(*[32]byte)(aunt)
	}
	wrappedProof := blobstreamxwrapper.BinaryMerkleProof{
		SideNodes: sideNodes,
		Key:       big.NewInt(proof.Index),
		NumLeaves: big.NewInt(proof.Total),
	}

	valid, err := blobstreamXwrapper.VerifyAttestation(
		&bind.CallOpts{},
		big.NewInt(int64(nonce)),
		tuple,
		wrappedProof,
	)
	if err != nil {
		return false, err
	}
	return valid, nil
}

2. Prueba de inclusión de transacciones

Para demostrar que una transacción de rollup es parte de la raíz de datos, tendremos que proporcionar dos pruebas: (1) una prueba Merkle de espacio de nombres de la transacción a una raíz de fila. Esto podría hacerse probando las acciones que contienen la transacción en la raíz de la fila utilizando una prueba Merkle de espacio de nombres. (2) Y, una prueba Merkle binaria de la raíz de fila a la raíz de datos.

Estas pruebas se pueden generar utilizando el ProveShares consulta.

Esto punto final permite consultar una prueba de acciones a raíces de fila, luego una raíz de fila a pruebas de raíz de datos. Se necesita un bloque height, un índice de acción inicial y un índice de acción final que definen un rango de acciones. Luego, se generan dos pruebas:

  • Una prueba NMT de las acciones a las raíces de las filas

  • Una prueba binaria de Merkle de la raíz de fila a la raíz de datos

NOTA

Si el rango de acciones abarca varias filas, entonces la prueba puede contener múltiples pruebas NMT y binarias.

El punto final se puede consultar utilizando el cliente golang:

ir

	sharesProof, err := trpc.ProveShares(ctx, 15, 0, 1)
	if err != nil {
		...
	}

Convertir las pruebas para ser utilizables en el DAVerifier biblioteca

Contratos inteligentes que utilizan el DAVerifier la biblioteca toma el siguiente formato de prueba:

solidez

/// @notice Contains the necessary parameters to prove that some shares, which were posted to
/// the Celestia network, were committed to by the BlobstreamX smart contract.
struct SharesProof {
    // The shares that were committed to.
    bytes[] data;
    // The shares proof to the row roots. If the shares span multiple rows, we will have multiple nmt proofs.
    NamespaceMerkleMultiproof[] shareProofs;
    // The namespace of the shares.
    Namespace namespace;
    // The rows where the shares belong. If the shares span multiple rows, we will have multiple rows.
    NamespaceNode[] rowRoots;
    // The proofs of the rowRoots to the data root.
    BinaryMerkleProof[] rowProofs;
    // The proof of the data root tuple to the data root tuple root that was posted to the BlobstreamX contract.
    AttestationProof attestationProof;
}

/// @notice Contains the necessary parameters needed to verify that a data root tuple
/// was committed to, by the BlobstreamX smart contract, at some specif nonce.
struct AttestationProof {
    // the attestation nonce that commits to the data root tuple.
    uint256 tupleRootNonce;
    // the data root tuple that was committed to.
    DataRootTuple tuple;
    // the binary Merkle proof of the tuple to the commitment.
    BinaryMerkleProof proof;
}

Para construir el SharesProof, necesitaremos la prueba que consultamos anteriormente, y es la siguiente:

data

Estas son las acciones en bruto que se presentaron a Celestia en el bytes formato. Si tomamos el ejemplo blob que se presentó en el RollupInclusionProofs.t.sol, podemos convertirlo en bytes usando el abi.encode(...) como hecho para esta variable. Esto se puede obtener del resultado anterior de la prueba de inclusión de transacciones consulta en el campo data.

shareProofs

Esta es la prueba de acciones a las raíces de las filas. Estos pueden contener múltiples pruebas si las acciones que contienen el blob se extienden a través de varias filas. Para construirlos, utilizaremos el resultado de la prueba de inclusión de transacciones sección.

Mientras que el NamespaceMerkleMultiproof ser:

solidez

/// @notice Namespace Merkle Tree Multiproof structure. Proves multiple leaves.
struct NamespaceMerkleMultiproof {
    // The beginning key of the leaves to verify.
    uint256 beginKey;
    // The ending key of the leaves to verify.
    uint256 endKey;
    // List of side nodes to verify and calculate tree.
    NamespaceNode[] sideNodes;
}

Entonces, podemos construir el NamespaceMerkleMultiproof con el siguiente mapeo:

  • beginKey en la estructura de Solidez == start en la respuesta de la consulta

  • endKey en la estructura de Solidez == end en la respuesta de la consulta

  • sideNodes en la estructura de Solidez == nodes en la respuesta de la consulta

  • El NamespaceNode, que es el tipo de sideNodes, se define de la siguiente manera:

solidez

/// @notice Namespace Merkle Tree node.
struct NamespaceNode {
    // Minimum namespace.
    Namespace min;
    // Maximum namespace.
    Namespace max;
    // Node value.
    bytes32 digest;
}

Entonces, construimos un NamespaceNode tomando los valores de la nodes campo en la respuesta de consulta, los convertimos de base64 a hex, luego usamos el siguiente mapeo:

  • min== los primeros 29 bytes en el valor decodificado

  • max== los segundos 29 bytes en el valor decodificado

  • digest== los 32 bytes restantes en el valor decodificado

El min y max son Namespace tipo que es:

solidez

/// @notice A representation of the Celestia-app namespace ID and its version.
/// See: https://celestiaorg.github.io/celestia-app/specs/namespace.html
struct Namespace {
    // The namespace version.
    bytes1 version;
    // The namespace ID.
    bytes28 id;
}

Entonces, para construirlos, separamos los 29 bytes en el valor decodificado para:

  • primer byte: version

  • 28 Bytes restantes: id

Un ejemplo de hacer esto se puede encontrar en el RollupInclusiónProofs.t.sol prueba.

Un ayudante golang que se puede utilizar para hacer esta conversión es el siguiente:

ir

func toNamespaceMerkleMultiProofs(proofs []*tmproto.NMTProof) []client.NamespaceMerkleMultiproof {
	shareProofs := make([]client.NamespaceMerkleMultiproof, len(proofs))
	for i, proof := range proofs {
		sideNodes := make([]client.NamespaceNode, len(proof.Nodes))
		for j, node := range proof.Nodes {
			sideNodes[j] = *toNamespaceNode(node)
		}
		shareProofs[i] = client.NamespaceMerkleMultiproof{
			BeginKey:  big.NewInt(int64(proof.Start)),
			EndKey:    big.NewInt(int64(proof.End)),
			SideNodes: sideNodes,
		}
	}
	return shareProofs
}

func minNamespace(innerNode []byte) *client.Namespace {
	version := innerNode[0]
	var id [28]byte
	for i, b := range innerNode[1:28] {
		id[i] = b
	}
	return &client.Namespace{
		Version: [1]byte{version},
		Id:      id,
	}
}

func maxNamespace(innerNode []byte) *client.Namespace {
	version := innerNode[29]
	var id [28]byte
	for i, b := range innerNode[30:57] {
		id[i] = b
	}
	return &client.Namespace{
		Version: [1]byte{version},
		Id:      id,
	}
}

func toNamespaceNode(node []byte) *client.NamespaceNode {
	minNs := minNamespace(node)
	maxNs := maxNamespace(node)
	var digest [32]byte
	for i, b := range node[58:] {
		digest[i] = b
	}
	return &client.NamespaceNode{
		Min:    *minNs,
		Max:    *maxNs,
		Digest: digest,
	}
}

con proofs ser sharesProof.ShareProofs.

namespace

Cuál es el espacio de nombres utilizado por el rollup al enviar datos a Celestia. Como se ha descrito anteriormente, se puede construir de la siguiente manera:

solidez

/// @notice A representation of the Celestia-app namespace ID and its version.
/// See: https://celestiaorg.github.io/celestia-app/specs/namespace.html
struct Namespace {
    // The namespace version.
    bytes1 version;
    // The namespace ID.
    bytes28 id;
}

A través de tomar el namespace valor de la prove_shares respuesta de consulta, decodificándola de base64 a hex, luego:

  • primer byte: version

  • 28 Bytes restantes: id

Un ejemplo se puede encontrar en el RollupInclusiónProofs.t.sol prueba.

Un método para convertir a espacio de nombres, siempre que el tamaño del espacio de nombres sea 29, es el siguiente:

ir

func namespace(namespaceID []byte) *client.Namespace {
	version := namespaceID[0]
	var id [28]byte
	for i, b := range namespaceID[1:] {
		id[i] = b
	}
	return &client.Namespace{
		Version: [1]byte{version},
		Id:      id,
	}
}

con namespace ser sharesProof.NamespaceID.

rowRoots

Cuáles son las raíces de las filas donde se localizan las acciones que contienen los datos de Rollup.

En golang, la prueba se puede convertir de la siguiente manera:

ir

func toRowRoots(roots []bytes.HexBytes) []client.NamespaceNode {
	rowRoots := make([]client.NamespaceNode, len(roots))
	for i, root := range roots {
		rowRoots[i] = *toNamespaceNode(root.Bytes())
	}
	return rowRoots
}

con roots ser sharesProof.RowProof.RowRoots.

rowProofs

Estas son las pruebas de las filas a la raíz de datos. Son de tipo BinaryMerkleProof:

solidez

/// @notice Merkle Tree Proof structure.
struct BinaryMerkleProof {
    // List of side nodes to verify and calculate tree.
    bytes32[] sideNodes;
    // The key of the leaf to verify.
    uint256 key;
    // The number of leaves in the tree
    uint256 numLeaves;
}

Para construirlos, tomamos la respuesta de la prove_shares consulta y realiza el siguiente mapeo:

  • key en la estructura de Solidez == index en la respuesta de la consulta

  • numLeaves en la estructura de Solidez == total en la respuesta de la consulta

  • sideNodes en la estructura de Solidez == aunts en la respuesta de la consulta

El tipo de la sideNodes es a bytes32.

Un ejemplo se puede encontrar en el RollupInclusiónProofs.t.sol prueba.

Un ayudante golang para convertir las pruebas de fila es el siguiente:

ir

func toRowProofs(proofs []*merkle.Proof) []client.BinaryMerkleProof {
	rowProofs := make([]client.BinaryMerkleProof, len(proofs))
	for i, proof := range proofs {
		sideNodes := make( [][32]byte, len(proof.Aunts))
		for j, sideNode :=  range proof.Aunts {
			var bzSideNode [32]byte
			for k, b := range sideNode {
				bzSideNode[k] = b
			}
			sideNodes[j] = bzSideNode
		}
 		rowProofs[i] = client.BinaryMerkleProof{
			SideNodes: sideNodes,
			Key:       big.NewInt(proof.Index),
			NumLeaves: big.NewInt(proof.Total),
		}
	}
}

con proofs ser sharesProof.RowProof.Proofs.

attestationProof

Esta es la prueba de la raíz de datos a la raíz de tupla de raíz de datos, que se compromete en el contrato de Blobstream X:

solidez

/// @notice Contains the necessary parameters needed to verify that a data root tuple
/// was committed to, by the BlobstreamX smart contract, at some specif nonce.
struct AttestationProof {
    // the attestation nonce that commits to the data root tuple.
    uint256 tupleRootNonce;
    // the data root tuple that was committed to.
    DataRootTuple tuple;
    // the binary Merkle proof of the tuple to the commitment.
    BinaryMerkleProof proof;
}
  • tupleRootNonce: el nonce en el que Blobstream X se comprometió con el lote que contiene el bloque que contiene los datos.

  • tuple: el DataRootTuple del bloque:

solidez

/// @notice A tuple of data root with metadata. Each data root is associated
///  with a Celestia block height.
/// @dev `availableDataRoot` in
///  https://github.com/celestiaorg/celestia-specs/blob/master/src/specs/data_structures.md#header
struct DataRootTuple {
    // Celestia block height the data root was included in.
    // Genesis block is height = 0.
    // First queryable block is height = 1.
    uint256 height;
    // Data root.
    bytes32 dataRoot;
}

que comprende a dataRoot, es decir, el bloque que contiene la raíz de datos de datos de Rollup, y el height que es el height de ese bloque.

  • proof: el BinaryMerkleProof de la tupla raíz de datos a la raíz de tupla raíz de datos. Construirlo es similar a construir las raíces de fila a prueba de raíz de datos en el rowProof sección.

Un ejemplo se puede encontrar en el RollupInclusiónProofs.t.sol prueba.

Un ayudante golang para crear una prueba de certificación:

ir

func toAttestationProof(
	nonce uint64,
	height uint64,
	blockDataRoot [32]byte,
	dataRootInclusionProof merkle.Proof,
) client.AttestationProof {
	sideNodes := make( [][32]byte, len(dataRootInclusionProof.Aunts))
	for i, sideNode :=  range dataRootInclusionProof.Aunts {
		var bzSideNode [32]byte
		for k, b := range sideNode {
			bzSideNode[k] = b
		}
		sideNodes[i] = bzSideNode
	}

	return client.AttestationProof{
		TupleRootNonce: big.NewInt(int64(nonce)),
		Tuple:          client.DataRootTuple{
			Height:   big.NewInt(int64(height)),
			DataRoot: blockDataRoot,
		},
		Proof:          client.BinaryMerkleProof{
			SideNodes: sideNodes,
			Key:       big.NewInt(dataRootInclusionProof.Index),
			NumLeaves: big.NewInt(dataRootInclusionProof.Total),
		},
	}
}

con el nonce siendo la certificación nonce, que se puede recuperar usando BlobstreamX eventos contractuales. Consulte a continuación un ejemplo. Y height siendo la altura del bloque Celestia que contiene los datos de despliegue, junto con el blockDataRoot siendo la raíz de datos de la altura del bloque. Finalmente, dataRootInclusionProof es la prueba de inclusión de raíz de datos de bloque Celestia a la raíz de tupla de raíz de datos que se consultó al comienzo de esta página.

Si el dataRoot o el tupleRootNonce se desconoce durante la verificación:

  • dataRoot: se puede consultar usando el /block?height=15 consulta (15 en este punto final de ejemplo), y tomando el data_hash campo de la respuesta.

  • tupleRootNonce: se puede volver a intentar consultando el BlobstreamXDataCommitmentStored eventos del contrato de BlobstreamX y buscando el nonce que acredite los datos correspondientes. Un ejemplo:

ir

	// get the nonce corresponding to the block height that contains the PayForBlob transaction
	// since BlobstreamX emits events when new batches are submitted, we will query the events
	// and look for the range committing to the blob
	// first, connect to an EVM RPC endpoint
	ethClient, err := ethclient.Dial("evm_rpc_endpoint")
	if err != nil {
		return err
	}
	defer ethClient.Close()

	// use the BlobstreamX contract binding
	wrapper, err := blobstreamxwrapper.NewBlobstreamX(ethcmn.HexToAddress("contract_Address"), ethClient)
	if err != nil {
		return err
	}

	LatestBlockNumber, err := ethClient.BlockNumber(ctx)
	if err != nil {
		return err
	}

	eventsIterator, err := wrapper.FilterDataCommitmentStored(
		&bind.FilterOpts{
			Context: ctx,
			Start: LatestBlockNumber - 90000, // 90000 can be replaced with the range of EVM blocks to look for the events in
			End: &LatestBlockNumber,
		},
		nil,
		nil,
		nil,
	)
	if err != nil {
		return err
	}

	var event *blobstreamxwrapper.BlobstreamXDataCommitmentStored
	for eventsIterator.Next() {
		e := eventsIterator.Event
		if int64(e.StartBlock) <= tx.Height && tx.Height < int64(e.EndBlock) {
			event = &blobstreamxwrapper.BlobstreamXDataCommitmentStored{
				ProofNonce:     e.ProofNonce,
				StartBlock:     e.StartBlock,
				EndBlock:       e.EndBlock,
				DataCommitment: e.DataCommitment,
			}
			break
		}
	}
	if err := eventsIterator.Error(); err != nil {
		return err
	}
	err = eventsIterator.Close()
	if err != nil {
		return err
	}
	if event == nil {
		return fmt.Errorf("couldn't find range containing the block height")
	}

Escuchar nuevos compromisos de datos

Para escuchar nuevo BlobstreamXDataCommitmentStored eventos, secuenciadores pueden usar el WatchDataCommitmentStored como sigue:

ir

    ethClient, err := ethclient.Dial("evm_rpc")
    if err != nil {
	    return err
    }
    defer ethClient.Close()
    blobstreamWrapper, err := blobstreamxwrapper.NewBlobstreamXFilterer(ethcmn.HexToAddress("contract_address"), ethClient)
    if err != nil {
	    return err
    }

    eventsChan := make(chan *blobstreamxwrapper.BlobstreamXDataCommitmentStored, 100)
    subscription, err := blobstreamWrapper.WatchDataCommitmentStored(
	    &bind.WatchOpts{
			Context: ctx,
        },
	    eventsChan,
	    nil,
	    nil,
	    nil,
	)
    if err != nil {
	    return err
    }
    defer subscription.Unsubscribe()

    for {
	    select {
	    case <-ctx.Done():
		    return ctx.Err()
		case err := <-subscription.Err():
			return err
		case event := <-eventsChan:
			// process the event
		    fmt.Println(event)
	    }
    }

Luego, se pueden crear nuevas pruebas como se documentó anteriormente utilizando los nuevos compromisos de datos contenidos en los eventos recibidos.

Ejemplo de rollup que utiliza el DAVerifier

Un ejemplo de rollup que utiliza el DAVerifier puede ser tan simple como:

solidez

pragma solidity ^0.8.22;

import {DAVerifier} from "@blobstream/lib/verifier/DAVerifier.sol";
import {IDAOracle} from "@blobstream/IDAOracle.sol";

contract SimpleRollup {
    IDAOracle bridge;
    ...
    function submitFraudProof(SharesProof memory _sharesProof, bytes32 _root) public {
        // (1) verify that the data is committed to by BlobstreamX contract
        (bool committedTo, DAVerifier.ErrorCodes err) = DAVerifier.verifySharesToDataRootTupleRoot(bridge, _sharesProof, _root);
        if (!committedTo) {
            revert("the data was not committed to by Blobstream");
        }
        // (2) verify that the data is part of the rollup block
        // (3) parse the data
        // (4) verify invalid state transition
        // (5) effects
    }
}

Luego, puede enviar la prueba de fraude usando golang de la siguiente manera:

ir

package main

import (
	"context"
	"fmt"
	"github.com/celestiaorg/celestia-app/pkg/square"
	"github.com/celestiaorg/celestia-app/x/qgb/client"
	"github.com/ethereum/go-ethereum/accounts/abi/bind"
	ethcmn "github.com/ethereum/go-ethereum/common"
	"github.com/ethereum/go-ethereum/ethclient"
	blobstreamxwrapper "github.com/succinctlabs/blobstreamx/bindings"
	"github.com/tendermint/tendermint/crypto/merkle"
	"github.com/tendermint/tendermint/libs/bytes"
	tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
	"github.com/tendermint/tendermint/rpc/client/http"
	"github.com/tendermint/tendermint/types"
	"math/big"
	"os"
)

func main() {
	err := verify()
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
}

func verify() error {
	ctx := context.Background()


	// ...
	// check the first section for this part of the implementation

	// get the nonce corresponding to the block height that contains the PayForBlob transaction
	// since Blobstream X emits events when new batches are submitted, we will query the events
	// and look for the range committing to the blob
	// first, connect to an EVM RPC endpoint
	ethClient, err := ethclient.Dial("evm_rpc_endpoint")
	if err != nil {
		return err
	}
	defer ethClient.Close()

	// ...
	// check the first section for this part of the implementation

	// now we will create the shares proof to be verified by the SimpleRollup
	// contract that uses the DAVerifier library

	// get the proof of the shares containing the blob to the data root
	sharesProof, err := trpc.ProveShares(ctx, 16, uint64(blobShareRange.Start), uint64(blobShareRange.End))
	if err != nil {
		return err
	}

	// use the SimpleRollup contract binding to submit to it a fraud proof
	simpleRollupWrapper, err := client.NewWrappers(ethcmn.HexToAddress("contract_Address"), ethClient)
	if err != nil {
		return err
	}

	// submit the fraud proof containing the share data that had the invalid state transition for example
	// along with its proof
	err = submitFraudProof(
		ctx,
		simpleRollupWrapper,
		sharesProof,
		event.ProofNonce.Uint64(),
		uint64(tx.Height),
		dcProof.Proof,
		blockRes.Block.DataHash,
	)

	return nil
}

func submitFraudProof(
	ctx context.Context,
	simpleRollup *client.Wrappers,
	sharesProof types.ShareProof,
	nonce uint64,
	height uint64,
	dataRootInclusionProof merkle.Proof,
	dataRoot []byte,
) error {
	var blockDataRoot [32]byte
	for i, b := range dataRoot[58:] {
		blockDataRoot[i] = b
	}
	tx, err := simpleRollup.SubmitFraudProof(
		&bind.TransactOpts{
			Context: ctx,
		},
		client.SharesProof{
			Data:             sharesProof.Data,
			ShareProofs:      toNamespaceMerkleMultiProofs(sharesProof.ShareProofs),
			Namespace:        *namespace(sharesProof.NamespaceID),
			RowRoots:         toRowRoots(sharesProof.RowProof.RowRoots),
			RowProofs:        toRowProofs(sharesProof.RowProof.Proofs),
			AttestationProof: toAttestationProof(nonce, height, blockDataRoot, dataRootInclusionProof),
		},
		blockDataRoot,
	)
	if err != nil {
		return err
	}
	// wait for transaction
}

func toAttestationProof(
	nonce uint64,
	height uint64,
	blockDataRoot [32]byte,
	dataRootInclusionProof merkle.Proof,
) client.AttestationProof {
	sideNodes := make( [][32]byte, len(dataRootInclusionProof.Aunts))
	for i, sideNode :=  range dataRootInclusionProof.Aunts {
		var bzSideNode [32]byte
		for k, b := range sideNode {
			bzSideNode[k] = b
		}
		sideNodes[i] = bzSideNode
	}

	return client.AttestationProof{
		TupleRootNonce: big.NewInt(int64(nonce)),
		Tuple:          client.DataRootTuple{
			Height:   big.NewInt(int64(height)),
			DataRoot: blockDataRoot,
		},
		Proof:          client.BinaryMerkleProof{
			SideNodes: sideNodes,
			Key:       big.NewInt(dataRootInclusionProof.Index),
			NumLeaves: big.NewInt(dataRootInclusionProof.Total),
		},
	}
}

func toRowRoots(roots []bytes.HexBytes) []client.NamespaceNode {
	rowRoots := make([]client.NamespaceNode, len(roots))
	for i, root := range roots {
		rowRoots[i] = *toNamespaceNode(root.Bytes())
	}
	return rowRoots
}

func toRowProofs(proofs []*merkle.Proof) []client.BinaryMerkleProof {
	rowProofs := make([]client.BinaryMerkleProof, len(proofs))
	for i, proof := range proofs {
		sideNodes := make( [][32]byte, len(proof.Aunts))
		for j, sideNode :=  range proof.Aunts {
			var bzSideNode [32]byte
			for k, b := range sideNode {
				bzSideNode[k] = b
			}
			sideNodes[j] = bzSideNode
		}
 		rowProofs[i] = client.BinaryMerkleProof{
			SideNodes: sideNodes,
			Key:       big.NewInt(proof.Index),
			NumLeaves: big.NewInt(proof.Total),
		}
	}
}

func toNamespaceMerkleMultiProofs(proofs []*tmproto.NMTProof) []client.NamespaceMerkleMultiproof {
	shareProofs := make([]client.NamespaceMerkleMultiproof, len(proofs))
	for i, proof := range proofs {
		sideNodes := make([]client.NamespaceNode, len(proof.Nodes))
		for j, node := range proof.Nodes {
			sideNodes[j] = *toNamespaceNode(node)
		}
		shareProofs[i] = client.NamespaceMerkleMultiproof{
			BeginKey:  big.NewInt(int64(proof.Start)),
			EndKey:    big.NewInt(int64(proof.End)),
			SideNodes: sideNodes,
		}
	}
	return shareProofs
}

func minNamespace(innerNode []byte) *client.Namespace {
	version := innerNode[0]
	var id [28]byte
	for i, b := range innerNode[1:28] {
		id[i] = b
	}
	return &client.Namespace{
		Version: [1]byte{version},
		Id:      id,
	}
}

func maxNamespace(innerNode []byte) *client.Namespace {
	version := innerNode[29]
	var id [28]byte
	for i, b := range innerNode[30:57] {
		id[i] = b
	}
	return &client.Namespace{
		Version: [1]byte{version},
		Id:      id,
	}
}

func toNamespaceNode(node []byte) *client.NamespaceNode {
	minNs := minNamespace(node)
	maxNs := maxNamespace(node)
	var digest [32]byte
	for i, b := range node[58:] {
		digest[i] = b
	}
	return &client.NamespaceNode{
		Min:    *minNs,
		Max:    *maxNs,
		Digest: digest,
	}
}

func namespace(namespaceID []byte) *client.Namespace {
	version := namespaceID[0]
	var id [28]byte
	for i, b := range namespaceID[1:] {
		id[i] = b
	}
	return &client.Namespace{
		Version: [1]byte{version},
		Id:      id,
	}
}

Para el paso (2), verifique el documentación de pruebas de inclusión de rollup para más información.

Conclusión

Después de crear todas las pruebas y verificarlas:

  1. Verifique la prueba de inclusión de la transacción a la raíz de datos de Celestia

  2. Demuestre que la tupla raíz de datos está comprometida con el contrato inteligente Blobstream X

Podemos estar seguros de que los datos se publicaron en Celestia, y luego las implementaciones pueden continuar con su mecanismo normal de prueba de fraude.

NOTA

Las construcciones de prueba anteriores se implementan en Solidity y pueden requerir diferentes enfoques en otros lenguajes de programación.

Last updated