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
})
}

// IsBlobNamespace returns a true if this namespace is a valid user-specifiable
// blob namespace.
func IsBlobNamespace(ns Namespace) bool {
cristaloleg 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
}
63 changes: 63 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
}

// MarshalJSON decodes json bytes to the namespace.
vgonkivs marked this conversation as resolved.
Show resolved Hide resolved
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 Expand Up @@ -203,6 +241,31 @@ func (n Namespace) Compare(n2 Namespace) int {
return bytes.Compare(n.data, n2.data)
}

// IsOutsideRange checks if the namespace is outside the min-max range of the given hashes.
func (n Namespace) IsOutsideRange(leftHash, rightHash []byte) bool {
if len(leftHash) < NamespaceSize || len(rightHash) < 2*NamespaceSize {
vgonkivs marked this conversation as resolved.
Show resolved Hide resolved
return false
}
return n.IsLessThan(Namespace{data: leftHash[:NamespaceSize]}) ||
!n.IsLessOrEqualThan(Namespace{data: rightHash[NamespaceSize : NamespaceSize*2]})
}

// IsAboveMax checks if the namespace is above the maximum namespace of the given hash.
func (n Namespace) IsAboveMax(hash []byte) bool {
if len(hash) < 2*NamespaceSize {
return false
}
return !n.IsLessOrEqualThan(Namespace{data: hash[NamespaceSize : NamespaceSize*2]})
}

// IsBelowMin checks if the target namespace is below the minimum namespace of the given hash.
func (n Namespace) IsBelowMin(hash []byte) bool {
if len(hash) < NamespaceSize {
return false
}
return n.IsLessThan(Namespace{data: hash[:NamespaceSize]})
}

// leftPad returns a new byte slice with the provided byte slice left-padded to the provided size.
// If the provided byte slice is already larger than the provided size, the original byte slice is returned.
func leftPad(b []byte, size int) []byte {
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,8 @@ package share

import (
"crypto/rand"
"slices"
"encoding/binary"
"errors"
)

func RandomNamespace() Namespace {
Expand Down Expand Up @@ -38,22 +39,55 @@ func RandomBlobNamespace() Namespace {
for {
id := RandomBlobNamespaceID()
namespace := MustNewV0Namespace(id)
if isBlobNamespace(namespace) {
if IsBlobNamespace(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
func AddInt(n Namespace, val int) (Namespace, error) {
vgonkivs marked this conversation as resolved.
Show resolved Hide resolved
if val == 0 {
return n, nil
}
// 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 {
return Namespace{}, errors.New("namespace overflow")
}

return true
return Namespace{data: result}, nil
}
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 generate 'total' amount of shares filled with random data.
vgonkivs marked this conversation as resolved.
Show resolved Hide resolved
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 same the as RandShares, but sets same namespace for all shares.
vgonkivs marked this conversation as resolved.
Show resolved Hide resolved
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)
vgonkivs marked this conversation as resolved.
Show resolved Hide resolved
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