diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d1147c6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +go.work +go.work.sum \ No newline at end of file diff --git a/blob/blob.go b/blob/blob.go index d3569e4..b3199b7 100644 --- a/blob/blob.go +++ b/blob/blob.go @@ -8,12 +8,12 @@ import ( "fmt" "sort" - "github.com/celestiaorg/go-square/namespace" + ns "github.com/celestiaorg/go-square/namespace" "google.golang.org/protobuf/proto" ) // SupportedBlobNamespaceVersions is a list of namespace versions that can be specified by a user for blobs. -var SupportedBlobNamespaceVersions = []uint8{namespace.NamespaceVersionZero} +var SupportedBlobNamespaceVersions = []uint8{ns.NamespaceVersionZero} // ProtoBlobTxTypeID is included in each encoded BlobTx to help prevent // decoding binaries that are not actually BlobTxs. @@ -28,21 +28,26 @@ const MaxShareVersion = 127 // New creates a new coretypes.Blob from the provided data after performing // basic stateless checks over it. -func New(ns namespace.Namespace, blob []byte, shareVersion uint8) *Blob { +func New(ns ns.Namespace, blob []byte, shareVersion uint8) *Blob { return &Blob{ - NamespaceId: ns.ID, + NamespaceId: ns.ID(), Data: blob, ShareVersion: uint32(shareVersion), - NamespaceVersion: uint32(ns.Version), + NamespaceVersion: uint32(ns.Version()), } } // Namespace returns the namespace of the blob -func (b *Blob) Namespace() namespace.Namespace { - return namespace.Namespace{ - Version: uint8(b.NamespaceVersion), - ID: b.NamespaceId, - } +func (b *Blob) Namespace() (ns.Namespace, error) { + return ns.NewFromBytes(b.RawNamespace()) +} + +// RawNamespace returns the namespace of the blob +func (b *Blob) RawNamespace() []byte { + namespace := make([]byte, ns.NamespaceSize) + namespace[ns.VersionIndex] = uint8(b.NamespaceVersion) + copy(namespace[ns.NamespaceVersionSize:], b.NamespaceId) + return namespace } // Validate runs a stateless validity check on the form of the struct. @@ -50,13 +55,13 @@ func (b *Blob) Validate() error { if b == nil { return errors.New("nil blob") } - if len(b.NamespaceId) != namespace.NamespaceIDSize { - return fmt.Errorf("namespace id must be %d bytes", namespace.NamespaceIDSize) + if len(b.NamespaceId) != ns.NamespaceIDSize { + return fmt.Errorf("namespace id must be %d bytes", ns.NamespaceIDSize) } if b.ShareVersion > MaxShareVersion { return errors.New("share version can not be greater than MaxShareVersion") } - if b.NamespaceVersion > namespace.NamespaceVersionMax { + if b.NamespaceVersion > ns.NamespaceVersionMax { return errors.New("namespace version can not be greater than MaxNamespaceVersion") } if len(b.Data) == 0 { @@ -65,6 +70,10 @@ func (b *Blob) Validate() error { return nil } +func (b *Blob) Compare(other *Blob) int { + return bytes.Compare(b.RawNamespace(), other.RawNamespace()) +} + // UnmarshalBlobTx attempts to unmarshal a transaction into blob transaction. If an // error is thrown, false is returned. func UnmarshalBlobTx(tx []byte) (*BlobTx, bool) { @@ -81,7 +90,7 @@ func UnmarshalBlobTx(tx []byte) (*BlobTx, bool) { return &bTx, false } for _, b := range bTx.Blobs { - if len(b.NamespaceId) != namespace.NamespaceIDSize { + if len(b.NamespaceId) != ns.NamespaceIDSize { return &bTx, false } } @@ -105,7 +114,7 @@ func MarshalBlobTx(tx []byte, blobs ...*Blob) ([]byte, error) { // Sort sorts the blobs by their namespace. func Sort(blobs []*Blob) { sort.SliceStable(blobs, func(i, j int) bool { - return bytes.Compare(blobs[i].Namespace().Bytes(), blobs[j].Namespace().Bytes()) < 0 + return blobs[i].Compare(blobs[j]) < 0 }) } diff --git a/inclusion/commitment.go b/inclusion/commitment.go index d84b4e2..4583dcf 100644 --- a/inclusion/commitment.go +++ b/inclusion/commitment.go @@ -20,7 +20,10 @@ func CreateCommitment(blob *blob.Blob, merkleRootFn MerkleRootFn, subtreeRootThr if err := blob.Validate(); err != nil { return nil, err } - namespace := blob.Namespace() + namespace, err := blob.Namespace() + if err != nil { + return nil, err + } shares, err := sh.SplitBlobs(blob) if err != nil { diff --git a/inclusion/commitment_test.go b/inclusion/commitment_test.go index 60d9b37..d77d34e 100644 --- a/inclusion/commitment_test.go +++ b/inclusion/commitment_test.go @@ -92,10 +92,10 @@ func TestCreateCommitment(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { blob := &blob.Blob{ - NamespaceId: tt.namespace.ID, + NamespaceId: tt.namespace.ID(), Data: tt.blob, ShareVersion: uint32(tt.shareVersion), - NamespaceVersion: uint32(tt.namespace.Version), + NamespaceVersion: uint32(tt.namespace.Version()), } res, err := inclusion.CreateCommitment(blob, twoLeafMerkleRoot, defaultSubtreeRootThreshold) if tt.expectErr { diff --git a/namespace/consts.go b/namespace/consts.go index bef6573..f56a85b 100644 --- a/namespace/consts.go +++ b/namespace/consts.go @@ -9,6 +9,10 @@ const ( // NamespaceVersionSize is the size of a namespace version in bytes. NamespaceVersionSize = 1 + // VersionIndex is the index of the version in the namespace. This should + // always be the first byte + VersionIndex = 0 + // NamespaceIDSize is the size of a namespace ID in bytes. NamespaceIDSize = 28 @@ -69,15 +73,9 @@ var ( ) func primaryReservedNamespace(lastByte byte) Namespace { - return Namespace{ - Version: NamespaceVersionZero, - ID: append(bytes.Repeat([]byte{0x00}, NamespaceIDSize-1), lastByte), - } + return newNamespace(NamespaceVersionZero, append(bytes.Repeat([]byte{0x00}, NamespaceIDSize-1), lastByte)) } func secondaryReservedNamespace(lastByte byte) Namespace { - return Namespace{ - Version: NamespaceVersionMax, - ID: append(bytes.Repeat([]byte{0xFF}, NamespaceIDSize-1), lastByte), - } + return newNamespace(NamespaceVersionMax, append(bytes.Repeat([]byte{0xFF}, NamespaceIDSize-1), lastByte)) } diff --git a/namespace/namespace.go b/namespace/namespace.go index f2e6cb6..7b83fad 100644 --- a/namespace/namespace.go +++ b/namespace/namespace.go @@ -7,8 +7,7 @@ import ( ) type Namespace struct { - Version uint8 - ID []byte + data []byte } // New returns a new namespace with the provided version and id. @@ -23,10 +22,16 @@ func New(version uint8, id []byte) (Namespace, error) { return Namespace{}, err } + return newNamespace(version, id), nil +} + +func newNamespace(version uint8, id []byte) Namespace { + data := make([]byte, NamespaceVersionSize+len(id)) + data[VersionIndex] = version + copy(data[NamespaceVersionSize:], id) return Namespace{ - Version: version, - ID: id, - }, nil + data: data, + } } // MustNew returns a new namespace with the provided version and id. It panics @@ -39,6 +44,27 @@ func MustNew(version uint8, id []byte) Namespace { return ns } +// NewFromBytes returns a new namespace from the provided byte slice. +func NewFromBytes(bytes []byte) (Namespace, error) { + if len(bytes) != NamespaceSize { + return Namespace{}, fmt.Errorf("invalid namespace length: %v must be %v", len(bytes), NamespaceSize) + } + + err := validateVersionSupported(bytes[VersionIndex]) + if err != nil { + return Namespace{}, err + } + + err = validateID(bytes[VersionIndex], bytes[NamespaceVersionSize:]) + if err != nil { + return Namespace{}, err + } + + return Namespace{ + data: bytes, + }, nil +} + // NewV0 returns a new namespace with version 0 and the provided subID. subID // must be <= 10 bytes. If subID is < 10 bytes, it will be left-padded with 0s // to fill 10 bytes. @@ -47,16 +73,10 @@ func NewV0(subID []byte) (Namespace, error) { return Namespace{}, fmt.Errorf("subID must be <= %v, but it was %v bytes", NamespaceVersionZeroIDSize, lenSubID) } - subID = leftPad(subID, NamespaceVersionZeroIDSize) - id := make([]byte, NamespaceIDSize) - copy(id[NamespaceVersionZeroPrefixSize:], subID) - - ns, err := New(NamespaceVersionZero, id) - if err != nil { - return Namespace{}, err - } + namespace := make([]byte, NamespaceSize) + copy(namespace[NamespaceSize-len(subID):], subID) - return ns, nil + return NewFromBytes(namespace) } // MustNewV0 returns a new namespace with version 0 and the provided subID. This @@ -70,18 +90,24 @@ func MustNewV0(subID []byte) Namespace { } // From returns a namespace from the provided byte slice. +// Deprecated: Please use NewFromBytes instead. func From(b []byte) (Namespace, error) { - if len(b) != NamespaceSize { - return Namespace{}, fmt.Errorf("invalid namespace length: %v must be %v", len(b), NamespaceSize) - } - rawVersion := b[0] - rawNamespace := b[1:] - return New(rawVersion, rawNamespace) + return NewFromBytes(b) } // Bytes returns this namespace as a byte slice. func (n Namespace) Bytes() []byte { - return append([]byte{n.Version}, n.ID...) + return n.data +} + +// Version return this namespace's version +func (n Namespace) Version() uint8 { + return n.data[VersionIndex] +} + +// ID returns this namespace's ID +func (n Namespace) ID() []byte { + return n.data[NamespaceVersionSize:] } // validateVersionSupported returns an error if the version is not supported. @@ -147,7 +173,7 @@ func (n Namespace) Repeat(times int) []Namespace { } func (n Namespace) Equals(n2 Namespace) bool { - return n.Version == n2.Version && bytes.Equal(n.ID, n2.ID) + return bytes.Equal(n.data, n2.data) } func (n Namespace) IsLessThan(n2 Namespace) bool { @@ -167,14 +193,7 @@ func (n Namespace) IsGreaterOrEqualThan(n2 Namespace) bool { } func (n Namespace) Compare(n2 Namespace) int { - switch { - case n.Version == n2.Version: - return bytes.Compare(n.ID, n2.ID) - case n.Version < n2.Version: - return -1 - default: - return 1 - } + return bytes.Compare(n.data, n2.data) } // leftPad returns a new byte slice with the provided byte slice left-padded to the provided size. @@ -190,14 +209,10 @@ func leftPad(b []byte, size int) []byte { // deepCopy returns a deep copy of the Namespace object. func (n Namespace) deepCopy() Namespace { // Create a deep copy of the ID slice - copyID := make([]byte, len(n.ID)) - copy(copyID, n.ID) + copyData := make([]byte, len(n.data)) + copy(copyData, n.data) - // Create a new Namespace object with the copied fields - copyNamespace := Namespace{ - Version: n.Version, - ID: copyID, + return Namespace{ + data: copyData, } - - return copyNamespace } diff --git a/namespace/namespace_test.go b/namespace/namespace_test.go index 0bf5430..ef96337 100644 --- a/namespace/namespace_test.go +++ b/namespace/namespace_test.go @@ -32,10 +32,7 @@ func TestNew(t *testing.T) { version: NamespaceVersionZero, id: validID, wantErr: false, - want: Namespace{ - Version: NamespaceVersionZero, - ID: validID, - }, + want: MustNew(NamespaceVersionZero, validID), }, { name: "unsupported version", @@ -76,19 +73,6 @@ func TestNew(t *testing.T) { } } -// TestRepeatNonMutability ensures that the output of Repeat method is not mutated when the original namespace is mutated. -func TestRepeatNonMutability(t *testing.T) { - n := 10 - namespace := Namespace{Version: NamespaceVersionMax, ID: []byte{1, 2, 3, 4}} - repeated := namespace.Repeat(n) - // mutate the original namespace - namespace.ID[0] = 5 - // ensure the repeated namespaces are not mutated - for i := 0; i < n; i++ { - assert.NotEqual(t, repeated[i], namespace) - } -} - func TestNewV0(t *testing.T) { type testCase struct { name string @@ -99,21 +83,15 @@ func TestNewV0(t *testing.T) { testCases := []testCase{ { - name: "valid namespace", - subID: bytes.Repeat([]byte{1}, NamespaceVersionZeroIDSize), - want: Namespace{ - Version: NamespaceVersionZero, - ID: append(NamespaceVersionZeroPrefix, bytes.Repeat([]byte{1}, NamespaceVersionZeroIDSize)...), - }, + name: "valid namespace", + subID: bytes.Repeat([]byte{1}, NamespaceVersionZeroIDSize), + want: MustNew(NamespaceVersionZero, append(NamespaceVersionZeroPrefix, bytes.Repeat([]byte{1}, NamespaceVersionZeroIDSize)...)), wantErr: false, }, { - name: "left pads subID if too short", - subID: []byte{1, 2, 3, 4}, - want: Namespace{ - Version: NamespaceVersionZero, - ID: append(NamespaceVersionZeroPrefix, []byte{0, 0, 0, 0, 0, 0, 1, 2, 3, 4}...), - }, + name: "left pads subID if too short", + subID: []byte{1, 2, 3, 4}, + want: MustNew(NamespaceVersionZero, append(NamespaceVersionZeroPrefix, []byte{0, 0, 0, 0, 0, 0, 1, 2, 3, 4}...)), wantErr: false, }, { @@ -153,19 +131,13 @@ func TestFrom(t *testing.T) { name: "valid namespace", bytes: validNamespace, wantErr: false, - want: Namespace{ - Version: NamespaceVersionZero, - ID: validID, - }, + want: MustNew(NamespaceVersionZero, validID), }, { name: "parity namespace", bytes: parityNamespace, wantErr: false, - want: Namespace{ - Version: NamespaceVersionMax, - ID: bytes.Repeat([]byte{0xFF}, NamespaceIDSize), - }, + want: MustNew(NamespaceVersionMax, bytes.Repeat([]byte{0xFF}, NamespaceIDSize)), }, { name: "unsupported version", @@ -285,10 +257,7 @@ func TestIsReserved(t *testing.T) { want: true, }, { - ns: Namespace{ - Version: math.MaxUint8, - ID: append(bytes.Repeat([]byte{0xFF}, NamespaceIDSize-1), 1), - }, + ns: MustNew(math.MaxUint8, append(bytes.Repeat([]byte{0xFF}, NamespaceIDSize-1), 1)), want: true, }, } @@ -308,7 +277,7 @@ func Test_compareMethods(t *testing.T) { } vers := []byte{NamespaceVersionZero, NamespaceVersionMax} - ids := [][]byte{minID, maxID} + ids := [][]byte{append(NamespaceVersionZeroPrefix, minID...), append(NamespaceVersionZeroPrefix, maxID...)} // collect all possible pairs: (ver1 ?? ver2) x (id1 ?? id2) var testPairs [][2]Namespace @@ -317,8 +286,8 @@ func Test_compareMethods(t *testing.T) { for _, id1 := range ids { for _, id2 := range ids { testPairs = append(testPairs, [2]Namespace{ - {Version: ver1, ID: id1}, - {Version: ver2, ID: id2}, + MustNew(ver1, id1), + MustNew(ver2, id2), }) } } diff --git a/namespace/random_blob.go b/namespace/random_blob.go index 702e88f..b8ec7a9 100644 --- a/namespace/random_blob.go +++ b/namespace/random_blob.go @@ -32,7 +32,7 @@ func isBlobNamespace(ns Namespace) bool { return false } - if !slices.Contains(SupportedBlobNamespaceVersions, ns.Version) { + if !slices.Contains(SupportedBlobNamespaceVersions, ns.Version()) { return false } diff --git a/shares/split_sparse_shares.go b/shares/split_sparse_shares.go index a84707a..ed6f26b 100644 --- a/shares/split_sparse_shares.go +++ b/shares/split_sparse_shares.go @@ -31,7 +31,10 @@ func (sss *SparseShareSplitter) Write(blob *blob.Blob) error { } rawData := blob.Data - blobNamespace := blob.Namespace() + blobNamespace, err := blob.Namespace() + if err != nil { + return err + } // First share (note by validating the blob we can safely cast the share version to uint8) b, err := NewBuilder(blobNamespace, uint8(blob.ShareVersion), true) diff --git a/square/builder.go b/square/builder.go index 9e5414d..9fea7aa 100644 --- a/square/builder.go +++ b/square/builder.go @@ -128,7 +128,9 @@ func (b *Builder) Export() (Square, error) { // of blobs within a namespace because b.Blobs are already ordered by tx // priority. sort.SliceStable(b.Blobs, func(i, j int) bool { - return bytes.Compare(b.Blobs[i].Blob.Namespace().Bytes(), b.Blobs[j].Blob.Namespace().Bytes()) < 0 + ns1 := append([]byte{byte(b.Blobs[i].Blob.NamespaceVersion)}, b.Blobs[i].Blob.NamespaceId...) + ns2 := append([]byte{byte(b.Blobs[j].Blob.NamespaceVersion)}, b.Blobs[j].Blob.NamespaceId...) + return bytes.Compare(ns1, ns2) < 0 }) // write all the regular transactions into compact shares