From e077a22249a07a70386bd65968a3029d826c4336 Mon Sep 17 00:00:00 2001
From: Hlib Kanunnikov <hlibwondertan@gmail.com>
Date: Wed, 21 Jun 2023 16:28:47 +0200
Subject: [PATCH] share: introduce Namespace type (#2367)

Mostly copied from the app's Namespace type with multiple modifications.
In preparation for
https://github.com/celestiaorg/celestia-node/issues/2301.

We want a more straightforward Namespace type from what the app
provides:
* Repsents 1:1 mapping of the serialized form of Namespace [version byte
+ namespace id]
* So that it does not allocate on the hot path when getting data by
namespace
* and had simpler json serialization
---
 share/namespace.go      | 151 ++++++++++++++++++++++++++++++++++++++++
 share/namespace_test.go |  84 ++++++++++++++++++++++
 share/nid.go            |   1 +
 3 files changed, 236 insertions(+)
 create mode 100644 share/namespace.go
 create mode 100644 share/namespace_test.go

diff --git a/share/namespace.go b/share/namespace.go
new file mode 100644
index 000000000..0af1d1a60
--- /dev/null
+++ b/share/namespace.go
@@ -0,0 +1,151 @@
+package share
+
+import (
+	"bytes"
+	"encoding/hex"
+	"fmt"
+
+	appns "github.com/celestiaorg/celestia-app/pkg/namespace"
+	"github.com/celestiaorg/nmt/namespace"
+)
+
+// Various reserved namespaces.
+var (
+	MaxReservedNamespace     = Namespace(appns.MaxReservedNamespace.Bytes())
+	ParitySharesNamespace    = Namespace(appns.ParitySharesNamespace.Bytes())
+	TailPaddingNamespace     = Namespace(appns.TailPaddingNamespace.Bytes())
+	ReservedPaddingNamespace = Namespace(appns.ReservedPaddingNamespace.Bytes())
+	TxNamespace              = Namespace(appns.TxNamespace.Bytes())
+	PayForBlobNamespace      = Namespace(appns.PayForBlobNamespace.Bytes())
+)
+
+// Namespace represents namespace of a Share.
+// Consists of version byte and namespace ID.
+type Namespace []byte
+
+// NamespaceFromBytes converts bytes into Namespace and validates it.
+func NamespaceFromBytes(b []byte) (Namespace, error) {
+	n := Namespace(b)
+	return n, n.Validate()
+}
+
+// Version reports version of the Namespace.
+func (n Namespace) Version() byte {
+	return n[appns.NamespaceVersionSize-1]
+}
+
+// ID reports ID of the Namespace.
+func (n Namespace) ID() namespace.ID {
+	return namespace.ID(n[appns.NamespaceVersionSize:])
+}
+
+// ToNMT converts the whole Namespace(both Version and ID parts) into NMT's namespace.ID
+// NOTE: Once https://github.com/celestiaorg/nmt/issues/206 is closed Namespace should become NNT's
+// type.
+func (n Namespace) ToNMT() namespace.ID {
+	return namespace.ID(n)
+}
+
+// ToAppNamespace converts the Namespace to App's definition of Namespace.
+// TODO: Unify types between node and app
+func (n Namespace) ToAppNamespace() appns.Namespace {
+	return appns.Namespace{Version: n.Version(), ID: n.ID()}
+}
+
+// Len reports the total length of the namespace.
+func (n Namespace) Len() int {
+	return len(n)
+}
+
+// String stringifies the Namespace.
+func (n Namespace) String() string {
+	return hex.EncodeToString(n)
+}
+
+// Equals compares two Namespaces.
+func (n Namespace) Equals(target Namespace) bool {
+	return bytes.Equal(n, target)
+}
+
+// Validate checks if the namespace is correct.
+func (n Namespace) Validate() error {
+	if n.Len() != NamespaceSize {
+		return fmt.Errorf("invalid namespace length: expected %d, got %d", NamespaceSize, n.Len())
+	}
+	if n.Version() != appns.NamespaceVersionZero && n.Version() != appns.NamespaceVersionMax {
+		return fmt.Errorf("invalid namespace version %v", n.Version())
+	}
+	if len(n.ID()) != appns.NamespaceIDSize {
+		return fmt.Errorf("invalid namespace id length: expected %d, got %d", appns.NamespaceIDSize, n.ID().Size())
+	}
+	if n.Version() == appns.NamespaceVersionZero && !bytes.HasPrefix(n.ID(), appns.NamespaceVersionZeroPrefix) {
+		return fmt.Errorf("invalid namespace id: expect %d leading zeroes", len(appns.NamespaceVersionZeroPrefix))
+	}
+	return nil
+}
+
+// ValidateDataNamespace checks if the Namespace contains real/useful data.
+func (n Namespace) ValidateDataNamespace() error {
+	if err := n.Validate(); err != nil {
+		return err
+	}
+	if n.Equals(ParitySharesNamespace) || n.Equals(TailPaddingNamespace) {
+		return fmt.Errorf("invalid data namespace(%s): parity and tail padding namespace are forbidden", n)
+	}
+	return nil
+}
+
+// ValidateBlobNamespace checks if the Namespace is valid blob namespace.
+func (n Namespace) ValidateBlobNamespace() error {
+	if err := n.ValidateDataNamespace(); err != nil {
+		return err
+	}
+	if bytes.Compare(n, MaxReservedNamespace) < 1 {
+		return fmt.Errorf("invalid blob namespace(%s): reserved namespaces are forbidden", n)
+	}
+	return nil
+}
+
+// IsAboveMax checks if the namespace is above the maximum namespace of the given hash.
+func (n Namespace) IsAboveMax(nodeHash []byte) bool {
+	return !n.IsLessOrEqual(nodeHash[n.Len() : n.Len()*2])
+}
+
+// IsBelowMin checks if the target namespace is below the minimum namespace of the given hash.
+func (n Namespace) IsBelowMin(nodeHash []byte) bool {
+	return n.IsLess(nodeHash[:n.Len()])
+}
+
+// IsOutsideRange checks if the namespace is outside the min-max range of the given hashes.
+func (n Namespace) IsOutsideRange(leftNodeHash, rightNodeHash []byte) bool {
+	return n.IsBelowMin(leftNodeHash) || n.IsAboveMax(rightNodeHash)
+}
+
+// Repeat copies the Namespace t times.
+func (n Namespace) Repeat(t int) []Namespace {
+	ns := make([]Namespace, t)
+	for i := 0; i < t; i++ {
+		ns[i] = n
+	}
+	return ns
+}
+
+// IsLess reports if the Namespace is less than the target.
+func (n Namespace) IsLess(target Namespace) bool {
+	return bytes.Compare(n, target) == -1
+}
+
+// IsLessOrEqual reports if the Namespace is less than the target.
+func (n Namespace) IsLessOrEqual(target Namespace) bool {
+	return bytes.Compare(n, target) < 1
+}
+
+// IsGreater reports if the Namespace is greater than the target.
+func (n Namespace) IsGreater(target Namespace) bool {
+	return bytes.Compare(n, target) == 1
+}
+
+// IsGreaterOrEqualThan reports if the Namespace is greater or equal than the target.
+func (n Namespace) IsGreaterOrEqualThan(target Namespace) bool {
+	return bytes.Compare(n, target) > -1
+}
diff --git a/share/namespace_test.go b/share/namespace_test.go
new file mode 100644
index 000000000..8cc61b379
--- /dev/null
+++ b/share/namespace_test.go
@@ -0,0 +1,84 @@
+package share
+
+import (
+	"bytes"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+
+	appns "github.com/celestiaorg/celestia-app/pkg/namespace"
+)
+
+var (
+	validID = append(
+		appns.NamespaceVersionZeroPrefix,
+		bytes.Repeat([]byte{1}, appns.NamespaceVersionZeroIDSize)...,
+	)
+	tooShortID      = append(appns.NamespaceVersionZeroPrefix, []byte{1}...)
+	tooLongID       = append(appns.NamespaceVersionZeroPrefix, bytes.Repeat([]byte{1}, NamespaceSize)...)
+	invalidPrefixID = bytes.Repeat([]byte{1}, NamespaceSize)
+)
+
+func TestFrom(t *testing.T) {
+	type testCase struct {
+		name    string
+		bytes   []byte
+		wantErr bool
+		want    Namespace
+	}
+	validNamespace := []byte{}
+	validNamespace = append(validNamespace, appns.NamespaceVersionZero)
+	validNamespace = append(validNamespace, appns.NamespaceVersionZeroPrefix...)
+	validNamespace = append(validNamespace, bytes.Repeat([]byte{0x1}, appns.NamespaceVersionZeroIDSize)...)
+	parityNamespace := bytes.Repeat([]byte{0xFF}, NamespaceSize)
+
+	testCases := []testCase{
+		{
+			name:    "valid namespace",
+			bytes:   validNamespace,
+			wantErr: false,
+			want:    append([]byte{appns.NamespaceVersionZero}, validID...),
+		},
+		{
+			name:    "parity namespace",
+			bytes:   parityNamespace,
+			wantErr: false,
+			want:    append([]byte{appns.NamespaceVersionMax}, bytes.Repeat([]byte{0xFF}, appns.NamespaceIDSize)...),
+		},
+		{
+			name: "unsupported version",
+			bytes: append([]byte{1}, append(
+				appns.NamespaceVersionZeroPrefix,
+				bytes.Repeat([]byte{1}, NamespaceSize-len(appns.NamespaceVersionZeroPrefix))...,
+			)...),
+			wantErr: true,
+		},
+		{
+			name:    "unsupported id: too short",
+			bytes:   append([]byte{appns.NamespaceVersionZero}, tooShortID...),
+			wantErr: true,
+		},
+		{
+			name:    "unsupported id: too long",
+			bytes:   append([]byte{appns.NamespaceVersionZero}, tooLongID...),
+			wantErr: true,
+		},
+		{
+			name:    "unsupported id: invalid prefix",
+			bytes:   append([]byte{appns.NamespaceVersionZero}, invalidPrefixID...),
+			wantErr: true,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			got, err := NamespaceFromBytes(tc.bytes)
+			if tc.wantErr {
+				assert.Error(t, err)
+				return
+			}
+			assert.NoError(t, err)
+			assert.Equal(t, tc.want, got)
+		})
+	}
+}
diff --git a/share/nid.go b/share/nid.go
index b7fd4e583..7d960cc9e 100644
--- a/share/nid.go
+++ b/share/nid.go
@@ -10,6 +10,7 @@ import (
 // NewNamespaceV0 takes a variable size byte slice and creates a version 0 Namespace ID.
 // The byte slice must be <= 10 bytes.
 // If it is less than 10 bytes, it will be left padded to size 10 with 0s.
+// TODO: Adapt for Namespace in the integration PR
 func NewNamespaceV0(subNId []byte) (namespace.ID, error) {
 	if lnid := len(subNId); lnid > appns.NamespaceVersionZeroIDSize {
 		return nil, fmt.Errorf("namespace id must be <= %v, but it was %v bytes", appns.NamespaceVersionZeroIDSize, lnid)