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
18 changes: 18 additions & 0 deletions share/blob.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package share
import (
"errors"
"fmt"
"slices"
"sort"

v1 "github.com/celestiaorg/go-square/v2/proto/blob/v1"
Expand Down Expand Up @@ -149,3 +150,20 @@ func SortBlobs(blobs []*Blob) {
return blobs[i].Compare(blobs[j]) < 0
})
}

// IsValidBlobNamespace returns a true if this namespace is a valid user-specifiable
vgonkivs marked this conversation as resolved.
Show resolved Hide resolved
// blob namespace.
func IsValidBlobNamespace(ns Namespace) bool {
vgonkivs marked this conversation as resolved.
Show resolved Hide resolved
if ns.IsReserved() {
return false
}

if !ns.IsUsableNamespace() {
return false
}

if !slices.Contains(SupportedBlobNamespaceVersions, ns.Version()) {
return false
}
return true
}
38 changes: 38 additions & 0 deletions share/namespace.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,35 @@ package share

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

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) {
Expand Down Expand Up @@ -92,6 +114,11 @@ 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
Expand All @@ -104,6 +131,17 @@ func ValidateUserNamespace(version uint8, id []byte) error {
return validateID(version, id)
}

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

// validateVersionSupported returns an error if the version is not supported.
func validateVersionSupported(version uint8) error {
if version != NamespaceVersionZero && version != NamespaceVersionMax {
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 IsValidBlobNamespace(namespace) {
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}
}
69 changes: 69 additions & 0 deletions share/random_shares.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
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 {
if total&(total-1) != 0 {
panic(fmt.Errorf("total must be power of 2: %d", total))
vgonkivs marked this conversation as resolved.
Show resolved Hide resolved
}

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 = ValidateForData(sh.Namespace()); 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
}

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

if namespacedAmount > total {
panic(fmt.Errorf("withNamespace must be less than total: %d", total))
vgonkivs marked this conversation as resolved.
Show resolved Hide resolved
}

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
}
15 changes: 15 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,20 @@ type Share struct {
data []byte
}

func (s Share) MarshalJSON() ([]byte, error) {
return json.Marshal(s.data)
}

func (s *Share) UnmarshalJSON(data []byte) error {
vgonkivs marked this conversation as resolved.
Show resolved Hide resolved
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
12 changes: 12 additions & 0 deletions share/share_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -238,3 +238,15 @@ func TestShareToBytesAndFromBytes(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, shares, reconstructedShares)
}

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

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

require.Equal(t, sh, newShare)
}
Loading