diff --git a/go.mod b/go.mod index 811dac2ce0..fbf5f2e145 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,9 @@ require ( connectrpc.com/connect v1.19.1 connectrpc.com/grpcreflect v1.3.0 github.com/celestiaorg/go-header v0.7.4 + github.com/celestiaorg/go-square/merkle v0.0.0-20240627094109-7d01436067a3 github.com/celestiaorg/go-square/v3 v3.0.2 + github.com/celestiaorg/nmt v0.24.2 github.com/celestiaorg/utils v0.1.0 github.com/evstack/ev-node/core v1.0.0-beta.5 github.com/go-kit/kit v0.13.0 diff --git a/go.sum b/go.sum index 5d60ca4218..1b73e27e93 100644 --- a/go.sum +++ b/go.sum @@ -26,8 +26,12 @@ github.com/celestiaorg/go-header v0.7.4 h1:kQx3bVvKV+H2etxRi4IUuby5VQydBONx3giHF github.com/celestiaorg/go-header v0.7.4/go.mod h1:eX9iTSPthVEAlEDLux40ZT/olXPGhpxHd+mEzJeDhd0= github.com/celestiaorg/go-libp2p-messenger v0.2.2 h1:osoUfqjss7vWTIZrrDSy953RjQz+ps/vBFE7bychLEc= github.com/celestiaorg/go-libp2p-messenger v0.2.2/go.mod h1:oTCRV5TfdO7V/k6nkx7QjQzGrWuJbupv+0o1cgnY2i4= +github.com/celestiaorg/go-square/merkle v0.0.0-20240627094109-7d01436067a3 h1:wP84mtwOCVNOTfS3zErICjxKLnh74Z1uf+tdrlSFjVM= +github.com/celestiaorg/go-square/merkle v0.0.0-20240627094109-7d01436067a3/go.mod h1:86qIYnEhmn/hfW+xvw98NOI3zGaDEB3x8JGjYo2FqLs= github.com/celestiaorg/go-square/v3 v3.0.2 h1:eSQOgNII8inK9IhiBZ+6GADQeWbRq4HYY72BOgcduA4= github.com/celestiaorg/go-square/v3 v3.0.2/go.mod h1:oFReMLsSDMRs82ICFEeFQFCqNvwdsbIM1BzCcb0f7dM= +github.com/celestiaorg/nmt v0.24.2 h1:LlpJSPOd6/Lw1Ig6HUhZuqiINHLka/ZSRTBzlNJpchg= +github.com/celestiaorg/nmt v0.24.2/go.mod h1:vgLBpWBi8F5KLxTdXSwb7AU4NhiIQ1AQRGa+PzdcLEA= github.com/celestiaorg/utils v0.1.0 h1:WsP3O8jF7jKRgLNFmlDCwdThwOFMFxg0MnqhkLFVxPo= github.com/celestiaorg/utils v0.1.0/go.mod h1:vQTh7MHnvpIeCQZ2/Ph+w7K1R2UerDheZbgJEJD2hSU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= @@ -123,6 +127,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= @@ -429,6 +435,12 @@ github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= diff --git a/pkg/blob/README.md b/pkg/blob/README.md new file mode 100644 index 0000000000..3424342da5 --- /dev/null +++ b/pkg/blob/README.md @@ -0,0 +1,18 @@ +# pkg/blob + +This package is a **trimmed copy** of code from `celestia-node` to stay JSON-compatible with the blob RPC without importing the full Cosmos/Celestia dependency set. + +## Upstream source +- `blob.go` comes from `celestia-node/blob/blob.go` @ tag `v0.28.4` (release v0.28.4), with unused pieces removed (blob v1, proof helpers, share length calc, appconsts dependency, etc.). +- `submit_options.go` mirrors the exported JSON fields of `celestia-node/state/tx_config.go` @ the same tag, leaving out functional options, defaults, and Cosmos keyring helpers. + +## Why copy instead of import? +- Avoids pulling Cosmos SDK / celestia-app dependencies into ev-node for the small surface we need (blob JSON and commitment for v0). +- Keeps binary size and module graph smaller while remaining wire-compatible with celestia-node's blob service. + +## Keeping it in sync +- When celestia-node changes blob JSON or tx config fields, update this package manually: + 1. `diff -u pkg/blob/blob.go ../Celestia/celestia-node/blob/blob.go` + 2. `diff -u pkg/blob/submit_options.go ../Celestia/celestia-node/state/tx_config.go` + 3. Port only the fields/logic required for our RPC surface. +- Consider adding a CI check that diffs these files against upstream to detect drift. diff --git a/pkg/blob/blob.go b/pkg/blob/blob.go new file mode 100644 index 0000000000..660ab0cc55 --- /dev/null +++ b/pkg/blob/blob.go @@ -0,0 +1,159 @@ +package blob + +// NOTE: This file is a trimmed copy of celestia-node's blob/blob.go +// at release v0.28.4 (commit tag v0.28.4). We keep only the JSON- +// compatible surface used by ev-node to avoid pulling celestia-app / +// Cosmos-SDK dependencies. See pkg/blob/README.md for update guidance. + +import ( + "bytes" + "encoding/binary" + "encoding/json" + "fmt" + + "github.com/celestiaorg/go-square/merkle" + "github.com/celestiaorg/go-square/v3/inclusion" + libshare "github.com/celestiaorg/go-square/v3/share" + "github.com/celestiaorg/nmt" +) + +// Commitment is the Merkle subtree commitment for a blob. +type Commitment []byte + +// Proof is a set of NMT proofs used to verify a blob inclusion. +// This mirrors celestia-node's blob.Proof shape. +type Proof []*nmt.Proof + +// DefaultMaxBlobSize is the default maximum blob size used by celestia-app (32 MiB). +const DefaultMaxBlobSize = 32 * 1_048_576 // bytes + +// subtreeRootThreshold is copied from celestia-app/v6 appconsts.SubtreeRootThreshold. +// It controls the branching factor when generating commitments. +const subtreeRootThreshold = 64 + +// Blob represents application-specific binary data that can be submitted to Celestia. +// It is intentionally compatible with celestia-node's blob.Blob JSON shape. +type Blob struct { + *libshare.Blob `json:"blob"` + + Commitment Commitment `json:"commitment"` + + // index is the index of the blob's first share in the EDS. + // Only blobs retrieved from the chain will have this set; default is -1. + index int +} + +// NewBlobV0 builds a version 0 blob (the only version we currently need). +func NewBlobV0(namespace libshare.Namespace, data []byte) (*Blob, error) { + return NewBlob(libshare.ShareVersionZero, namespace, data, nil) +} + +// NewBlob constructs a new blob from the provided namespace, data, signer, and share version. +// This is a lightly adapted copy of celestia-node/blob.NewBlob. +func NewBlob(shareVersion uint8, namespace libshare.Namespace, data, signer []byte) (*Blob, error) { + if err := namespace.ValidateForBlob(); err != nil { + return nil, fmt.Errorf("invalid namespace: %w", err) + } + + libBlob, err := libshare.NewBlob(namespace, data, shareVersion, signer) + if err != nil { + return nil, fmt.Errorf("build blob: %w", err) + } + + com, err := inclusion.CreateCommitment(libBlob, merkle.HashFromByteSlices, subtreeRootThreshold) + if err != nil { + return nil, fmt.Errorf("create commitment: %w", err) + } + + return &Blob{ + Blob: libBlob, + Commitment: com, + index: -1, + }, nil +} + +// Namespace returns the blob namespace. +func (b *Blob) Namespace() libshare.Namespace { + return b.Blob.Namespace() +} + +// Index returns the blob's first share index in the EDS (or -1 if unknown). +func (b *Blob) Index() int { + return b.index +} + +// MarshalJSON matches celestia-node's blob JSON encoding. +func (b *Blob) MarshalJSON() ([]byte, error) { + type jsonBlob struct { + Namespace []byte `json:"namespace"` + Data []byte `json:"data"` + ShareVersion uint8 `json:"share_version"` + Commitment Commitment `json:"commitment"` + Signer []byte `json:"signer,omitempty"` + Index int `json:"index"` + } + + jb := &jsonBlob{ + Namespace: b.Namespace().Bytes(), + Data: b.Data(), + ShareVersion: b.ShareVersion(), + Commitment: b.Commitment, + Signer: b.Signer(), + Index: b.index, + } + return json.Marshal(jb) +} + +// UnmarshalJSON matches celestia-node's blob JSON decoding. +func (b *Blob) UnmarshalJSON(data []byte) error { + type jsonBlob struct { + Namespace []byte `json:"namespace"` + Data []byte `json:"data"` + ShareVersion uint8 `json:"share_version"` + Commitment Commitment `json:"commitment"` + Signer []byte `json:"signer,omitempty"` + Index int `json:"index"` + } + + var jb jsonBlob + if err := json.Unmarshal(data, &jb); err != nil { + return err + } + + ns, err := libshare.NewNamespaceFromBytes(jb.Namespace) + if err != nil { + return err + } + + blob, err := NewBlob(jb.ShareVersion, ns, jb.Data, jb.Signer) + if err != nil { + return err + } + + blob.Commitment = jb.Commitment + blob.index = jb.Index + *b = *blob + return nil +} + +// MakeID constructs a blob ID by prefixing the commitment with the height (little endian). +func MakeID(height uint64, commitment Commitment) []byte { + id := make([]byte, 8+len(commitment)) + binary.LittleEndian.PutUint64(id, height) + copy(id[8:], commitment) + return id +} + +// SplitID splits a blob ID into height and commitment. +// If the ID is malformed, it returns height 0 and nil commitment. +func SplitID(id []byte) (uint64, Commitment) { + if len(id) <= 8 { + return 0, nil + } + return binary.LittleEndian.Uint64(id[:8]), id[8:] +} + +// EqualCommitment compares the blob's commitment with the provided one. +func (b *Blob) EqualCommitment(com Commitment) bool { + return bytes.Equal(b.Commitment, com) +} diff --git a/pkg/blob/blob_test.go b/pkg/blob/blob_test.go new file mode 100644 index 0000000000..2949fe4dd0 --- /dev/null +++ b/pkg/blob/blob_test.go @@ -0,0 +1,34 @@ +package blob + +import ( + "encoding/json" + "testing" + + libshare "github.com/celestiaorg/go-square/v3/share" + "github.com/stretchr/testify/require" +) + +func TestMakeAndSplitID(t *testing.T) { + id := MakeID(42, []byte{0x01, 0x02, 0x03}) + height, com := SplitID(id) + require.Equal(t, uint64(42), height) + require.Equal(t, []byte{0x01, 0x02, 0x03}, []byte(com)) +} + +func TestBlobJSONRoundTrip(t *testing.T) { + ns := libshare.MustNewV0Namespace([]byte("test-ids")) + + blob, err := NewBlobV0(ns, []byte("hello")) + require.NoError(t, err) + require.NotEmpty(t, blob.Commitment) + + encoded, err := json.Marshal(blob) + require.NoError(t, err) + + var decoded Blob + require.NoError(t, json.Unmarshal(encoded, &decoded)) + + require.Equal(t, blob.Namespace().Bytes(), decoded.Namespace().Bytes()) + require.Equal(t, blob.Data(), decoded.Data()) + require.Equal(t, blob.Commitment, decoded.Commitment) +} diff --git a/pkg/blob/submit_options.go b/pkg/blob/submit_options.go new file mode 100644 index 0000000000..b49db50fca --- /dev/null +++ b/pkg/blob/submit_options.go @@ -0,0 +1,27 @@ +package blob + +// NOTE: This mirrors the exported JSON shape of celestia-node/state/tx_config.go +// at release v0.28.4, pared down to avoid importing Cosmos-SDK and +// celestia-app packages. See pkg/blob/README.md for rationale and sync tips. + +// TxPriority mirrors celestia-node/state.TxPriority to preserve JSON compatibility. +type TxPriority int + +const ( + TxPriorityLow TxPriority = iota + 1 + TxPriorityMedium + TxPriorityHigh +) + +// SubmitOptions is a pared-down copy of celestia-node/state.TxConfig JSON shape. +// Only exported fields are marshalled to match the RPC expectation of the blob service. +type SubmitOptions struct { + GasPrice float64 `json:"gas_price,omitempty"` + IsGasPriceSet bool `json:"is_gas_price_set,omitempty"` + MaxGasPrice float64 `json:"max_gas_price,omitempty"` + Gas uint64 `json:"gas,omitempty"` + TxPriority TxPriority `json:"tx_priority,omitempty"` + KeyName string `json:"key_name,omitempty"` + SignerAddress string `json:"signer_address,omitempty"` + FeeGranterAddress string `json:"fee_granter_address,omitempty"` +}