Skip to content

Commit a129efc

Browse files
author
tac0turtle
committed
add celestia spec namespace
1 parent 560b41b commit a129efc

File tree

4 files changed

+515
-35
lines changed

4 files changed

+515
-35
lines changed

da/jsonrpc/client.go

Lines changed: 15 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"github.com/rs/zerolog"
1212

1313
"github.com/evstack/ev-node/core/da"
14+
dautils "github.com/evstack/ev-node/da"
1415
internal "github.com/evstack/ev-node/da/jsonrpc/internal"
1516
)
1617

@@ -36,28 +37,10 @@ type API struct {
3637
}
3738
}
3839

39-
// prepareNamespace hex encodes the namespace and ensures result is exactly 29 bytes
40-
func prepareNamespace(ns []byte) []byte {
41-
// Hex encode the namespace
42-
encoded := hex.EncodeToString(ns)
43-
44-
// Ensure the result is exactly 29 bytes
45-
// If shorter, pad with zeros; if longer, truncate
46-
if len(encoded) < 29 {
47-
// Pad with zeros on the right
48-
encoded = encoded + strings.Repeat("0", 29-len(encoded))
49-
} else if len(encoded) > 29 {
50-
// Truncate to 29 bytes
51-
encoded = encoded[:29]
52-
}
53-
54-
return []byte(encoded)
55-
}
56-
5740
// Get returns Blob for each given ID, or an error.
5841
func (api *API) Get(ctx context.Context, ids []da.ID, ns []byte) ([]da.Blob, error) {
59-
preparedNs := prepareNamespace(ns)
60-
api.Logger.Debug().Str("method", "Get").Int("num_ids", len(ids)).Str("namespace", string(preparedNs)).Msg("Making RPC call")
42+
preparedNs := dautils.PrepareNamespace(ns)
43+
api.Logger.Debug().Str("method", "Get").Int("num_ids", len(ids)).Str("namespace", hex.EncodeToString(preparedNs)).Msg("Making RPC call")
6144
res, err := api.Internal.Get(ctx, ids, preparedNs)
6245
if err != nil {
6346
if strings.Contains(err.Error(), context.Canceled.Error()) {
@@ -74,8 +57,8 @@ func (api *API) Get(ctx context.Context, ids []da.ID, ns []byte) ([]da.Blob, err
7457

7558
// GetIDs returns IDs of all Blobs located in DA at given height.
7659
func (api *API) GetIDs(ctx context.Context, height uint64, ns []byte) (*da.GetIDsResult, error) {
77-
preparedNs := prepareNamespace(ns)
78-
api.Logger.Debug().Str("method", "GetIDs").Uint64("height", height).Str("namespace", string(preparedNs)).Msg("Making RPC call")
60+
preparedNs := dautils.PrepareNamespace(ns)
61+
api.Logger.Debug().Str("method", "GetIDs").Uint64("height", height).Str("namespace", hex.EncodeToString(preparedNs)).Msg("Making RPC call")
7962
res, err := api.Internal.GetIDs(ctx, height, preparedNs)
8063
if err != nil {
8164
// Using strings.contains since JSON RPC serialization doesn't preserve error wrapping
@@ -108,8 +91,8 @@ func (api *API) GetIDs(ctx context.Context, height uint64, ns []byte) (*da.GetID
10891

10992
// GetProofs returns inclusion Proofs for Blobs specified by their IDs.
11093
func (api *API) GetProofs(ctx context.Context, ids []da.ID, ns []byte) ([]da.Proof, error) {
111-
preparedNs := prepareNamespace(ns)
112-
api.Logger.Debug().Str("method", "GetProofs").Int("num_ids", len(ids)).Str("namespace", string(preparedNs)).Msg("Making RPC call")
94+
preparedNs := dautils.PrepareNamespace(ns)
95+
api.Logger.Debug().Str("method", "GetProofs").Int("num_ids", len(ids)).Str("namespace", hex.EncodeToString(preparedNs)).Msg("Making RPC call")
11396
res, err := api.Internal.GetProofs(ctx, ids, preparedNs)
11497
if err != nil {
11598
api.Logger.Error().Err(err).Str("method", "GetProofs").Msg("RPC call failed")
@@ -121,8 +104,8 @@ func (api *API) GetProofs(ctx context.Context, ids []da.ID, ns []byte) ([]da.Pro
121104

122105
// Commit creates a Commitment for each given Blob.
123106
func (api *API) Commit(ctx context.Context, blobs []da.Blob, ns []byte) ([]da.Commitment, error) {
124-
preparedNs := prepareNamespace(ns)
125-
api.Logger.Debug().Str("method", "Commit").Int("num_blobs", len(blobs)).Str("namespace", string(preparedNs)).Msg("Making RPC call")
107+
preparedNs := dautils.PrepareNamespace(ns)
108+
api.Logger.Debug().Str("method", "Commit").Int("num_blobs", len(blobs)).Str("namespace", hex.EncodeToString(preparedNs)).Msg("Making RPC call")
126109
res, err := api.Internal.Commit(ctx, blobs, preparedNs)
127110
if err != nil {
128111
api.Logger.Error().Err(err).Str("method", "Commit").Msg("RPC call failed")
@@ -134,8 +117,8 @@ func (api *API) Commit(ctx context.Context, blobs []da.Blob, ns []byte) ([]da.Co
134117

135118
// Validate validates Commitments against the corresponding Proofs. This should be possible without retrieving the Blobs.
136119
func (api *API) Validate(ctx context.Context, ids []da.ID, proofs []da.Proof, ns []byte) ([]bool, error) {
137-
preparedNs := prepareNamespace(ns)
138-
api.Logger.Debug().Str("method", "Validate").Int("num_ids", len(ids)).Int("num_proofs", len(proofs)).Str("namespace", string(preparedNs)).Msg("Making RPC call")
120+
preparedNs := dautils.PrepareNamespace(ns)
121+
api.Logger.Debug().Str("method", "Validate").Int("num_ids", len(ids)).Int("num_proofs", len(proofs)).Str("namespace", hex.EncodeToString(preparedNs)).Msg("Making RPC call")
139122
res, err := api.Internal.Validate(ctx, ids, proofs, preparedNs)
140123
if err != nil {
141124
api.Logger.Error().Err(err).Str("method", "Validate").Msg("RPC call failed")
@@ -147,8 +130,8 @@ func (api *API) Validate(ctx context.Context, ids []da.ID, proofs []da.Proof, ns
147130

148131
// Submit submits the Blobs to Data Availability layer.
149132
func (api *API) Submit(ctx context.Context, blobs []da.Blob, gasPrice float64, ns []byte) ([]da.ID, error) {
150-
preparedNs := prepareNamespace(ns)
151-
api.Logger.Debug().Str("method", "Submit").Int("num_blobs", len(blobs)).Float64("gas_price", gasPrice).Str("namespace", string(preparedNs)).Msg("Making RPC call")
133+
preparedNs := dautils.PrepareNamespace(ns)
134+
api.Logger.Debug().Str("method", "Submit").Int("num_blobs", len(blobs)).Float64("gas_price", gasPrice).Str("namespace", hex.EncodeToString(preparedNs)).Msg("Making RPC call")
152135
res, err := api.Internal.Submit(ctx, blobs, gasPrice, preparedNs)
153136
if err != nil {
154137
if strings.Contains(err.Error(), context.Canceled.Error()) {
@@ -188,8 +171,8 @@ func (api *API) SubmitWithOptions(ctx context.Context, inputBlobs []da.Blob, gas
188171
return nil, da.ErrBlobSizeOverLimit
189172
}
190173

191-
preparedNs := prepareNamespace(ns)
192-
api.Logger.Debug().Str("method", "SubmitWithOptions").Int("num_blobs", len(inputBlobs)).Uint64("total_size", totalSize).Float64("gas_price", gasPrice).Str("namespace", string(preparedNs)).Msg("Making RPC call")
174+
preparedNs := dautils.PrepareNamespace(ns)
175+
api.Logger.Debug().Str("method", "SubmitWithOptions").Int("num_blobs", len(inputBlobs)).Uint64("total_size", totalSize).Float64("gas_price", gasPrice).Str("namespace", hex.EncodeToString(preparedNs)).Msg("Making RPC call")
193176
res, err := api.Internal.SubmitWithOptions(ctx, inputBlobs, gasPrice, preparedNs, options)
194177
if err != nil {
195178
if strings.Contains(err.Error(), context.Canceled.Error()) {

da/jsonrpc/proxy_test.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"testing"
1111
"time"
1212

13+
"github.com/evstack/ev-node/da"
1314
"github.com/evstack/ev-node/da/internal/mocks"
1415
proxy "github.com/evstack/ev-node/da/jsonrpc"
1516
"github.com/rs/zerolog"
@@ -232,9 +233,9 @@ func HeightFromFutureTest(t *testing.T, d coreda.DA) {
232233
func TestSubmitWithOptions(t *testing.T) {
233234
ctx := context.Background()
234235
testNamespace := []byte("options_test")
235-
// The client will hex encode testNamespace and pad/truncate to 29 bytes
236-
// "options_test" hex encoded is "6f7074696f6e735f74657374" (24 chars), padded to 29 with zeros
237-
encodedNamespace := []byte("6f7074696f6e735f7465737400000")
236+
// The client will convert the namespace string to a proper Celestia namespace
237+
// using SHA256 hashing and version 0 format (1 version byte + 28 ID bytes)
238+
encodedNamespace := da.PrepareNamespace(testNamespace)
238239
testOptions := []byte("test_options")
239240
gasPrice := 0.0
240241

da/namespace.go

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
package da
2+
3+
import (
4+
"crypto/sha256"
5+
"encoding/hex"
6+
"fmt"
7+
"strings"
8+
)
9+
10+
// Implemented in accordance to https://celestiaorg.github.io/celestia-app/namespace.html
11+
12+
const (
13+
// NamespaceVersionSize is the size of the namespace version in bytes
14+
NamespaceVersionSize = 1
15+
// NamespaceIDSize is the size of the namespace ID in bytes
16+
NamespaceIDSize = 28
17+
// NamespaceSize is the total size of a namespace (version + ID) in bytes
18+
NamespaceSize = NamespaceVersionSize + NamespaceIDSize
19+
20+
// NamespaceVersionZero is the only supported user-specifiable namespace version
21+
NamespaceVersionZero = uint8(0)
22+
// NamespaceVersionMax is the max namespace version
23+
NamespaceVersionMax = uint8(255)
24+
25+
// NamespaceVersionZeroPrefixSize is the number of leading zero bytes required for version 0
26+
NamespaceVersionZeroPrefixSize = 18
27+
// NamespaceVersionZeroDataSize is the number of data bytes available for version 0
28+
NamespaceVersionZeroDataSize = 10
29+
)
30+
31+
// Namespace represents a Celestia namespace
32+
type Namespace struct {
33+
Version uint8
34+
ID [NamespaceIDSize]byte
35+
}
36+
37+
// Bytes returns the namespace as a byte slice
38+
func (n Namespace) Bytes() []byte {
39+
result := make([]byte, NamespaceSize)
40+
result[0] = n.Version
41+
copy(result[1:], n.ID[:])
42+
return result
43+
}
44+
45+
// IsValidForVersion0 checks if the namespace is valid for version 0
46+
// Version 0 requires the first 18 bytes of the ID to be zero
47+
func (n Namespace) IsValidForVersion0() bool {
48+
if n.Version != NamespaceVersionZero {
49+
return false
50+
}
51+
52+
for i := 0; i < NamespaceVersionZeroPrefixSize; i++ {
53+
if n.ID[i] != 0 {
54+
return false
55+
}
56+
}
57+
return true
58+
}
59+
60+
// NewNamespaceV0 creates a new version 0 namespace from the provided data
61+
// The data should be up to 10 bytes and will be placed in the last 10 bytes of the ID
62+
// The first 18 bytes will be zeros as required by the specification
63+
func NewNamespaceV0(data []byte) (*Namespace, error) {
64+
if len(data) > NamespaceVersionZeroDataSize {
65+
return nil, fmt.Errorf("data too long for version 0 namespace: got %d bytes, max %d",
66+
len(data), NamespaceVersionZeroDataSize)
67+
}
68+
69+
ns := &Namespace{
70+
Version: NamespaceVersionZero,
71+
}
72+
73+
// The first 18 bytes are already zero (Go zero-initializes)
74+
// Copy the data to the last 10 bytes
75+
copy(ns.ID[NamespaceVersionZeroPrefixSize:], data)
76+
77+
return ns, nil
78+
}
79+
80+
// NamespaceFromBytes creates a namespace from a 29-byte slice
81+
func NamespaceFromBytes(b []byte) (*Namespace, error) {
82+
if len(b) != NamespaceSize {
83+
return nil, fmt.Errorf("invalid namespace size: expected %d, got %d", NamespaceSize, len(b))
84+
}
85+
86+
ns := &Namespace{
87+
Version: b[0],
88+
}
89+
copy(ns.ID[:], b[1:])
90+
91+
// Validate if it's version 0
92+
if ns.Version == NamespaceVersionZero && !ns.IsValidForVersion0() {
93+
return nil, fmt.Errorf("invalid version 0 namespace: first %d bytes of ID must be zero",
94+
NamespaceVersionZeroPrefixSize)
95+
}
96+
97+
return ns, nil
98+
}
99+
100+
// NamespaceFromString creates a version 0 namespace from a string identifier
101+
// The string is hashed and the first 10 bytes of the hash are used as the namespace data
102+
func NamespaceFromString(s string) *Namespace {
103+
// Hash the string to get consistent bytes
104+
hash := sha256.Sum256([]byte(s))
105+
106+
// Use the first 10 bytes of the hash for the namespace data
107+
ns, _ := NewNamespaceV0(hash[:NamespaceVersionZeroDataSize])
108+
return ns
109+
}
110+
111+
// HexString returns the hex representation of the namespace
112+
func (n Namespace) HexString() string {
113+
return "0x" + hex.EncodeToString(n.Bytes())
114+
}
115+
116+
// ParseHexNamespace parses a hex string into a namespace
117+
func ParseHexNamespace(hexStr string) (*Namespace, error) {
118+
// Remove 0x prefix if present
119+
hexStr = strings.TrimPrefix(hexStr, "0x")
120+
121+
b, err := hex.DecodeString(hexStr)
122+
if err != nil {
123+
return nil, fmt.Errorf("invalid hex string: %w", err)
124+
}
125+
126+
return NamespaceFromBytes(b)
127+
}
128+
129+
// PrepareNamespace converts a namespace identifier (string or bytes) into a proper Celestia namespace
130+
// This is the main function to be used when preparing namespaces for DA operations
131+
func PrepareNamespace(identifier []byte) []byte {
132+
// If the identifier is already a valid namespace (29 bytes), validate and return it
133+
if len(identifier) == NamespaceSize {
134+
ns, err := NamespaceFromBytes(identifier)
135+
if err == nil {
136+
return ns.Bytes()
137+
}
138+
// If it's not a valid namespace, treat it as a string identifier
139+
}
140+
141+
// Convert the identifier to a string and create a namespace from it
142+
ns := NamespaceFromString(string(identifier))
143+
return ns.Bytes()
144+
}

0 commit comments

Comments
 (0)