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.
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.
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.
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:
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.
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:
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:
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:
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.
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:
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:
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:
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,
}
}
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
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 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")
}
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,
}
}
Después de crear todas las pruebas y verificarlas:
Verifique la prueba de inclusión de la transacción a la raíz de datos de Celestia
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.