Skip to content

Commit 9d7e856

Browse files
authored
feat: new ParseShare API (#828)
Closes #723 Opens #839
1 parent e088d61 commit 9d7e856

File tree

3 files changed

+211
-0
lines changed

3 files changed

+211
-0
lines changed

pkg/shares/share_merging.go

+41
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ package shares
22

33
import (
44
"bytes"
5+
"fmt"
56

67
"github.com/celestiaorg/celestia-app/pkg/appconsts"
8+
"github.com/celestiaorg/nmt/namespace"
79
"github.com/celestiaorg/rsmt2d"
810
"github.com/gogo/protobuf/proto"
911
tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
@@ -132,3 +134,42 @@ func ParseMsgs(shares [][]byte) (coretypes.Messages, error) {
132134
MessagesList: msgList,
133135
}, nil
134136
}
137+
138+
// ShareSequence represents a contiguous sequence of shares that are part of the
139+
// same namespace and message. For compact shares, one share sequence exists per
140+
// reserved namespace. For sparse shares, one share sequence exists per message.
141+
type ShareSequence struct {
142+
NamespaceID namespace.ID
143+
Shares []Share
144+
}
145+
146+
func ParseShares(rawShares [][]byte) (result []ShareSequence, err error) {
147+
currentSequence := ShareSequence{}
148+
149+
for _, rawShare := range rawShares {
150+
share, err := NewShare(rawShare)
151+
if err != nil {
152+
return result, err
153+
}
154+
infoByte, err := share.InfoByte()
155+
if err != nil {
156+
return result, err
157+
}
158+
if infoByte.IsMessageStart() {
159+
if len(currentSequence.Shares) > 0 {
160+
result = append(result, currentSequence)
161+
}
162+
currentSequence = ShareSequence{
163+
Shares: []Share{share},
164+
NamespaceID: share.NamespaceID(),
165+
}
166+
} else {
167+
if !bytes.Equal(currentSequence.NamespaceID, share.NamespaceID()) {
168+
return result, fmt.Errorf("share sequence %v has inconsistent namespace IDs with share %v", currentSequence, share)
169+
}
170+
currentSequence.Shares = append(currentSequence.Shares, share)
171+
}
172+
}
173+
174+
return result, nil
175+
}

pkg/shares/share_merging_test.go

+163
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
package shares
2+
3+
import (
4+
"math/rand"
5+
"reflect"
6+
"testing"
7+
8+
"github.com/celestiaorg/celestia-app/pkg/appconsts"
9+
"github.com/celestiaorg/nmt/namespace"
10+
tmrand "github.com/tendermint/tendermint/libs/rand"
11+
"github.com/tendermint/tendermint/types"
12+
)
13+
14+
func TestParseShares(t *testing.T) {
15+
type testCase struct {
16+
name string
17+
shares [][]byte
18+
want []ShareSequence
19+
expectErr bool
20+
}
21+
22+
start := true
23+
messageOneNamespace := namespace.ID{1, 1, 1, 1, 1, 1, 1, 1}
24+
messageTwoNamespace := namespace.ID{2, 2, 2, 2, 2, 2, 2, 2}
25+
26+
transactionShares := SplitTxs(generateRandomTxs(2, 1000))
27+
transactionShareStart := transactionShares[0]
28+
transactionShareContinuation := transactionShares[1]
29+
30+
messageOneShares, err := SplitMessages(0, []uint32{}, []types.Message{generateRandomMessageWithNamespace(messageOneNamespace, 1000)}, false)
31+
if err != nil {
32+
t.Fatal(err)
33+
}
34+
messageOneStart := messageOneShares[0]
35+
messageOneContinuation := messageOneShares[1]
36+
37+
messageTwoShares, err := SplitMessages(0, []uint32{}, []types.Message{generateRandomMessageWithNamespace(messageTwoNamespace, 1000)}, false)
38+
if err != nil {
39+
t.Fatal(err)
40+
}
41+
messageTwoStart := messageTwoShares[0]
42+
messageTwoContinuation := messageTwoShares[1]
43+
44+
invalidShare := generateRawShare(messageOneNamespace, start)
45+
invalidShare = append(invalidShare, []byte{0}...)
46+
47+
tests := []testCase{
48+
{
49+
"empty",
50+
[][]byte{},
51+
[]ShareSequence{},
52+
false,
53+
},
54+
{
55+
"one transaction share",
56+
[][]byte{transactionShareStart},
57+
[]ShareSequence{{NamespaceID: appconsts.TxNamespaceID, Shares: []Share{transactionShareStart}}},
58+
false,
59+
},
60+
{
61+
"two transaction shares",
62+
[][]byte{transactionShareStart, transactionShareContinuation},
63+
[]ShareSequence{{NamespaceID: appconsts.TxNamespaceID, Shares: []Share{transactionShareStart, transactionShareContinuation}}},
64+
false,
65+
},
66+
{
67+
"one message share",
68+
[][]byte{messageOneStart},
69+
[]ShareSequence{{NamespaceID: messageOneNamespace, Shares: []Share{messageOneStart}}},
70+
false,
71+
},
72+
{
73+
"two message shares",
74+
[][]byte{messageOneStart, messageOneContinuation},
75+
[]ShareSequence{{NamespaceID: messageOneNamespace, Shares: []Share{messageOneStart, messageOneContinuation}}},
76+
false,
77+
},
78+
{
79+
"two messages with two shares each",
80+
[][]byte{messageOneStart, messageOneContinuation, messageTwoStart, messageTwoContinuation},
81+
[]ShareSequence{
82+
{NamespaceID: messageOneNamespace, Shares: []Share{messageOneStart, messageOneContinuation}},
83+
{NamespaceID: messageTwoNamespace, Shares: []Share{messageTwoStart, messageTwoContinuation}},
84+
},
85+
false,
86+
},
87+
{
88+
"one transaction, one message",
89+
[][]byte{transactionShareStart, messageOneStart},
90+
[]ShareSequence{
91+
{NamespaceID: appconsts.TxNamespaceID, Shares: []Share{transactionShareStart}},
92+
{NamespaceID: messageOneNamespace, Shares: []Share{messageOneStart}},
93+
},
94+
false,
95+
},
96+
{
97+
"one transaction, two messages",
98+
[][]byte{transactionShareStart, messageOneStart, messageTwoStart},
99+
[]ShareSequence{
100+
{NamespaceID: appconsts.TxNamespaceID, Shares: []Share{transactionShareStart}},
101+
{NamespaceID: messageOneNamespace, Shares: []Share{messageOneStart}},
102+
{NamespaceID: messageTwoNamespace, Shares: []Share{messageTwoStart}},
103+
},
104+
false,
105+
},
106+
{
107+
"one share with invalid size",
108+
[][]byte{invalidShare},
109+
[]ShareSequence{},
110+
true,
111+
},
112+
{
113+
"message one start followed by message two continuation",
114+
[][]byte{messageOneStart, messageTwoContinuation},
115+
[]ShareSequence{},
116+
true,
117+
},
118+
}
119+
for _, tt := range tests {
120+
t.Run(tt.name, func(t *testing.T) {
121+
got, err := ParseShares(tt.shares)
122+
if tt.expectErr && err == nil {
123+
t.Errorf("ParseShares() error %v, expectErr %v", err, tt.expectErr)
124+
}
125+
if reflect.DeepEqual(got, tt.want) {
126+
t.Errorf("ParseShares() got %v, want %v", got, tt.want)
127+
}
128+
})
129+
}
130+
}
131+
132+
func generateRawShare(namespace namespace.ID, isMessageStart bool) (rawShare []byte) {
133+
infoByte, _ := NewInfoByte(appconsts.ShareVersion, isMessageStart)
134+
rawData := make([]byte, appconsts.ShareSize-len(rawShare))
135+
rand.Read(rawData)
136+
137+
rawShare = append(rawShare, namespace...)
138+
rawShare = append(rawShare, byte(infoByte))
139+
rawShare = append(rawShare, rawData...)
140+
141+
return rawShare
142+
}
143+
144+
func generateRandomTxs(count, size int) types.Txs {
145+
txs := make(types.Txs, count)
146+
for i := 0; i < count; i++ {
147+
tx := make([]byte, size)
148+
_, err := rand.Read(tx)
149+
if err != nil {
150+
panic(err)
151+
}
152+
txs[i] = tx
153+
}
154+
return txs
155+
}
156+
157+
func generateRandomMessageWithNamespace(namespace namespace.ID, size int) types.Message {
158+
msg := types.Message{
159+
NamespaceID: namespace,
160+
Data: tmrand.Bytes(size),
161+
}
162+
return msg
163+
}

pkg/shares/shares.go

+7
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@ import (
1212
// Share contains the raw share data (including namespace ID).
1313
type Share []byte
1414

15+
func NewShare(data []byte) (Share, error) {
16+
if len(data) != appconsts.ShareSize {
17+
return nil, fmt.Errorf("share data must be %d bytes, got %d", appconsts.ShareSize, len(data))
18+
}
19+
return Share(data), nil
20+
}
21+
1522
func (s Share) NamespaceID() namespace.ID {
1623
if len(s) < appconsts.NamespaceSize {
1724
panic(fmt.Sprintf("share %s is too short to contain a namespace ID", s))

0 commit comments

Comments
 (0)