Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add missing utilities used by celestia-node #109

Merged
merged 15 commits into from
Oct 17, 2024
10 changes: 10 additions & 0 deletions share/blob.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,3 +149,13 @@ func SortBlobs(blobs []*Blob) {
return blobs[i].Compare(blobs[j]) < 0
})
}

// ToShares converts blob's data back to share.
func (b *Blob) ToShares() ([]Share, error) {
splitter := NewSparseShareSplitter()
err := splitter.Write(b)
if err != nil {
return nil, err
}
return splitter.Export(), nil
}
9 changes: 9 additions & 0 deletions share/blob_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,15 @@ func TestBlobConstructor(t *testing.T) {
_, err = NewBlob(ns2, data, 0, nil)
require.Error(t, err)
require.Contains(t, err.Error(), "namespace version must be 0")

blob, err := NewBlob(ns, data, 0, nil)
require.NoError(t, err)
shares, err := blob.ToShares()
require.NoError(t, err)
blobList, err := parseSparseShares(shares)
require.NoError(t, err)
require.Len(t, blobList, 1)
require.Equal(t, blob, blobList[0])
}

func TestNewBlobFromProto(t *testing.T) {
Expand Down
93 changes: 71 additions & 22 deletions share/namespace.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,41 @@ package share

import (
"bytes"
"encoding/hex"
"encoding/json"
"fmt"
"slices"
)

type Namespace struct {
data []byte
}

// MarshalJSON encodes namespace to the json encoded bytes.
func (n Namespace) MarshalJSON() ([]byte, error) {
return json.Marshal(n.data)
Wondertan marked this conversation as resolved.
Show resolved Hide resolved
}

// UnmarshalJSON decodes json bytes to the namespace.
func (n *Namespace) UnmarshalJSON(data []byte) error {
var buf []byte
if err := json.Unmarshal(data, &buf); err != nil {
return err
}

ns, err := NewNamespaceFromBytes(buf)
if err != nil {
return err
}
*n = ns
return nil
}

// NewNamespace validates the provided version and id and returns a new namespace.
// This should be used for user specified namespaces.
func NewNamespace(version uint8, id []byte) (Namespace, error) {
if err := ValidateUserNamespace(version, id); err != nil {
return Namespace{}, err
}

return newNamespace(version, id), nil
ns := newNamespace(version, id)
return ns, ns.ValidateUserNamespace()
}
vgonkivs marked this conversation as resolved.
Show resolved Hide resolved

func newNamespace(version uint8, id []byte) Namespace {
Expand Down Expand Up @@ -44,13 +64,9 @@ func NewNamespaceFromBytes(bytes []byte) (Namespace, error) {
if len(bytes) != NamespaceSize {
return Namespace{}, fmt.Errorf("invalid namespace length: %d. Must be %d bytes", len(bytes), NamespaceSize)
}
if err := ValidateUserNamespace(bytes[VersionIndex], bytes[NamespaceVersionSize:]); err != nil {
return Namespace{}, err
}

return Namespace{
data: bytes,
}, nil
ns := Namespace{data: bytes}
return ns, ns.ValidateUserNamespace()
}

// NewV0Namespace returns a new namespace with version 0 and the provided subID. subID
Expand Down Expand Up @@ -92,35 +108,51 @@ func (n Namespace) ID() []byte {
return n.data[NamespaceVersionSize:]
}

// String stringifies the Namespace.
func (n Namespace) String() string {
return hex.EncodeToString(n.data)
}

// ValidateUserNamespace returns an error if the provided version is not
// supported or the provided id does not meet the requirements
// for the provided version. This should be used for validating
// user specified namespaces
func ValidateUserNamespace(version uint8, id []byte) error {
err := validateVersionSupported(version)
func (n Namespace) ValidateUserNamespace() error {
Wondertan marked this conversation as resolved.
Show resolved Hide resolved
err := n.validateVersionSupported()
if err != nil {
return err
}
return validateID(version, id)
return n.validateID()
}

// ValidateForData checks if the Namespace is of real/useful data.
func (n Namespace) ValidateForData() error {
vgonkivs marked this conversation as resolved.
Show resolved Hide resolved
if err := n.ValidateUserNamespace(); err != nil {
return err
}
vgonkivs marked this conversation as resolved.
Show resolved Hide resolved
if !n.IsUsableNamespace() {
return fmt.Errorf("invalid data namespace(%s): parity and tail padding namespace are forbidden", n)
}
return nil
}
vgonkivs marked this conversation as resolved.
Show resolved Hide resolved

// validateVersionSupported returns an error if the version is not supported.
func validateVersionSupported(version uint8) error {
if version != NamespaceVersionZero && version != NamespaceVersionMax {
return fmt.Errorf("unsupported namespace version %v", version)
func (n Namespace) validateVersionSupported() error {
if n.Version() != NamespaceVersionZero && n.Version() != NamespaceVersionMax {
return fmt.Errorf("unsupported namespace version %v", n.Version())
}
return nil
}

// validateID returns an error if the provided id does not meet the requirements
// for the provided version.
func validateID(version uint8, id []byte) error {
if len(id) != NamespaceIDSize {
return fmt.Errorf("unsupported namespace id length: id %v must be %v bytes but it was %v bytes", id, NamespaceIDSize, len(id))
func (n Namespace) validateID() error {
if len(n.ID()) != NamespaceIDSize {
return fmt.Errorf("unsupported namespace id length: id %v must be %v bytes but it was %v bytes", n.ID(), NamespaceIDSize, len(n.ID()))
}

if version == NamespaceVersionZero && !bytes.HasPrefix(id, NamespaceVersionZeroPrefix) {
return fmt.Errorf("unsupported namespace id with version %v. ID %v must start with %v leading zeros", version, id, len(NamespaceVersionZeroPrefix))
if n.Version() == NamespaceVersionZero && !bytes.HasPrefix(n.ID(), NamespaceVersionZeroPrefix) {
return fmt.Errorf("unsupported namespace id with version %v. ID %v must start with %v leading zeros", n.Version(), n.ID(), len(NamespaceVersionZeroPrefix))
}
return nil
}
Expand Down Expand Up @@ -179,6 +211,23 @@ func (n Namespace) Repeat(times int) []Namespace {
return ns
}

// IsBlobNamespace returns true if this namespace is a valid user-specifiable
// blob namespace.
func (n Namespace) IsBlobNamespace() bool {
if err := n.ValidateForData(); err != nil {
return false
}

if n.IsReserved() {
return false
}

if !slices.Contains(SupportedBlobNamespaceVersions, n.Version()) {
return false
}
return true
}

func (n Namespace) Equals(n2 Namespace) bool {
return bytes.Equal(n.data, n2.data)
}
Expand Down
12 changes: 12 additions & 0 deletions share/namespace_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,18 @@ func Test_compareMethods(t *testing.T) {
}
}

func TestMarshalNamespace(t *testing.T) {
ns := RandomNamespace()
b, err := ns.MarshalJSON()
require.NoError(t, err)

newNs := Namespace{}
err = newNs.UnmarshalJSON(b)
require.NoError(t, err)

require.Equal(t, ns, newNs)
}

func BenchmarkEqual(b *testing.B) {
n1 := RandomNamespace()
n2 := RandomNamespace()
Expand Down
54 changes: 44 additions & 10 deletions share/random_namespace.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ package share

import (
"crypto/rand"
"slices"
"encoding/binary"
"testing"

"github.com/stretchr/testify/assert"
)

func RandomNamespace() Namespace {
Expand Down Expand Up @@ -38,22 +41,53 @@ func RandomBlobNamespace() Namespace {
for {
id := RandomBlobNamespaceID()
namespace := MustNewV0Namespace(id)
if isBlobNamespace(namespace) {
if namespace.IsBlobNamespace() {
return namespace
}
}
}

// isBlobNamespace returns an true if this namespace is a valid user-specifiable
// blob namespace.
func isBlobNamespace(ns Namespace) bool {
if ns.IsReserved() {
return false
// AddInt adds arbitrary int value to namespace, treating namespace as big-endian
// implementation of int. Note: should only be used for tests.
func AddInt(t *testing.T, n Namespace, val int) Namespace {
walldiss marked this conversation as resolved.
Show resolved Hide resolved
assert.Greater(t, val, 0)
// Convert the input integer to a byte slice and add it to result slice
result := make([]byte, NamespaceSize)
if val > 0 {
binary.BigEndian.PutUint64(result[NamespaceSize-8:], uint64(val))
} else {
binary.BigEndian.PutUint64(result[NamespaceSize-8:], uint64(-val))
}

// Perform addition byte by byte
var carry int
nn := n.Bytes()
for i := NamespaceSize - 1; i >= 0; i-- {
var sum int
if val > 0 {
sum = int(nn[i]) + int(result[i]) + carry
} else {
sum = int(nn[i]) - int(result[i]) + carry
}

switch {
case sum > 255:
carry = 1
sum -= 256
case sum < 0:
carry = -1
sum += 256
default:
carry = 0
}

result[i] = uint8(sum)
}

if !slices.Contains(SupportedBlobNamespaceVersions, ns.Version()) {
return false
// Handle any remaining carry
if carry != 0 {
t.Fatal("namespace overflow")
}

return true
return Namespace{data: result}
}
70 changes: 70 additions & 0 deletions share/random_shares.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package share

import (
"bytes"
"crypto/rand"
"fmt"
"sort"
)

// RandShares generates total amount of shares and fills them with random data.
func RandShares(total int) ([]Share, error) {
if total&(total-1) != 0 {
return nil, fmt.Errorf("total must be power of 2: %d", total)
}

shares := make([]Share, total)
for i := range shares {
shr := make([]byte, ShareSize)
copy(shr[:NamespaceSize], RandomNamespace().Bytes())
if _, err := rand.Read(shr[NamespaceSize:]); err != nil {
panic(err)
}

sh, err := NewShare(shr)
if err != nil {
panic(err)
}
if err = sh.Namespace().ValidateForData(); err != nil {
panic(err)
}

shares[i] = *sh
}
sort.Slice(shares, func(i, j int) bool { return bytes.Compare(shares[i].ToBytes(), shares[j].ToBytes()) < 0 })
return shares, nil
}

// RandSharesWithNamespace is the same as RandShares, but sets the same namespace for all shares.
func RandSharesWithNamespace(namespace Namespace, namespacedAmount, total int) ([]Share, error) {
if total&(total-1) != 0 {
return nil, fmt.Errorf("total must be power of 2: %d", total)
}

if namespacedAmount > total {
return nil,
fmt.Errorf("namespacedAmount %v must be less than or equal to total: %v", namespacedAmount, total)
}

shares := make([]Share, total)
for i := range shares {
shr := make([]byte, ShareSize)
if i < namespacedAmount {
copy(shr[:NamespaceSize], namespace.Bytes())
} else {
copy(shr[:NamespaceSize], RandomNamespace().Bytes())
}
_, err := rand.Read(shr[NamespaceSize:])
if err != nil {
panic(err)
}

sh, err := NewShare(shr)
if err != nil {
panic(err)
}
shares[i] = *sh
}
sort.Slice(shares, func(i, j int) bool { return bytes.Compare(shares[i].ToBytes(), shares[j].ToBytes()) < 0 })
return shares, nil
}
17 changes: 17 additions & 0 deletions share/share.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ package share
import (
"bytes"
"encoding/binary"
"encoding/json"
"fmt"
)

Expand All @@ -17,6 +18,22 @@ type Share struct {
data []byte
}

// MarshalJSON encodes share to the json encoded bytes.
func (s Share) MarshalJSON() ([]byte, error) {
return json.Marshal(s.data)
}

// UnmarshalJSON decodes json bytes to the share.
func (s *Share) UnmarshalJSON(data []byte) error {
var buf []byte

if err := json.Unmarshal(data, &buf); err != nil {
return err
}
s.data = buf
return validateSize(s.data)
Wondertan marked this conversation as resolved.
Show resolved Hide resolved
}

// NewShare creates a new share from the raw data, validating it's
// size and versioning
func NewShare(data []byte) (*Share, error) {
Expand Down
13 changes: 13 additions & 0 deletions share/share_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -238,3 +238,16 @@ func TestShareToBytesAndFromBytes(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, shares, reconstructedShares)
}

func TestMarshalShare(t *testing.T) {
sh, err := RandShares(1)
require.NoError(t, err)
b, err := sh[0].MarshalJSON()
require.NoError(t, err)

newShare := Share{}
err = newShare.UnmarshalJSON(b)
require.NoError(t, err)

require.Equal(t, sh[0], newShare)
}
Loading