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

Spec compliant merge shares #261

Merged
merged 35 commits into from
Apr 6, 2021
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
494db15
start spec compliant share merging
evan-forbes Mar 31, 2021
0d15df4
refactor and finish unit testing
evan-forbes Apr 1, 2021
3f0d808
whoops
evan-forbes Apr 1, 2021
8535122
linter gods
evan-forbes Apr 1, 2021
8bae88b
fix initial changes and use constants
evan-forbes Apr 1, 2021
cbb5d76
use constant
evan-forbes Apr 1, 2021
b523831
more polish
evan-forbes Apr 1, 2021
f0f38a6
docs fix
evan-forbes Apr 1, 2021
014a00e
review feedback: docs and out of range panic protection
evan-forbes Apr 1, 2021
0896a3b
review feedback: add panic protection from empty input
evan-forbes Apr 1, 2021
96eafc7
use constant instead of recalculating `ShareSize`
evan-forbes Apr 1, 2021
c3d897a
don't redeclare existing var
evan-forbes Apr 1, 2021
28f9769
be more explicit with returned nil
evan-forbes Apr 1, 2021
9ff16f5
use constant instead of recalculating `ShareSize`
evan-forbes Apr 1, 2021
39ce26a
review feedback: use consistent capitalization
evan-forbes Apr 1, 2021
8794411
stop accepting reserved namespaces as normal messages
evan-forbes Apr 1, 2021
2b28c59
use a descriptive var name for message length
evan-forbes Apr 1, 2021
d118716
linter and comparison fix
evan-forbes Apr 1, 2021
3ae1f79
reorg tests, add test for parse delimiter, DataFromBlock and fix evid…
evan-forbes Apr 2, 2021
dfe2a07
catch error for linter
evan-forbes Apr 3, 2021
e9f3a2e
update test MakeShares to include length delimiters for the SHARE_RES…
evan-forbes Apr 3, 2021
221b0fb
minor iteration change
evan-forbes Apr 3, 2021
37316e8
refactor share splitting to fix bug
evan-forbes Apr 3, 2021
e3736d6
fix all bugs with third and final refactor
evan-forbes Apr 4, 2021
632bb37
Merge branch 'master' into evan/merge-shares
evan-forbes Apr 4, 2021
3ca2afa
fix conflict
evan-forbes Apr 4, 2021
0f930fd
revert unnecessary changes
evan-forbes Apr 4, 2021
91c3989
review feedback: better docs
evan-forbes Apr 5, 2021
043812d
reivew feedback: add comment for safeLen
evan-forbes Apr 5, 2021
76d7a4b
review feedback: remove unnecessay comments
evan-forbes Apr 5, 2021
1cb4030
review feedback: split up share merging and splitting into their own …
evan-forbes Apr 5, 2021
a88db3b
review feedback: more descriptive var names
evan-forbes Apr 5, 2021
2aad8fd
fix accidental change
evan-forbes Apr 5, 2021
887aa08
add some constant docs
evan-forbes Apr 5, 2021
40a57c1
spelling error
evan-forbes Apr 5, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions types/consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ var (
TxNamespaceID = namespace.ID{0, 0, 0, 0, 0, 0, 0, 1}
IntermediateStateRootsNamespaceID = namespace.ID{0, 0, 0, 0, 0, 0, 0, 2}
EvidenceNamespaceID = namespace.ID{0, 0, 0, 0, 0, 0, 0, 3}
MaxReservedNamespace = namespace.ID{0, 0, 0, 0, 0, 0, 0, 255}
liamsi marked this conversation as resolved.
Show resolved Hide resolved

TailPaddingNamespaceID = namespace.ID{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE}
ParitySharesNamespaceID = namespace.ID{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}
Expand Down
331 changes: 325 additions & 6 deletions types/shares.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ import (
"bytes"
"encoding/binary"

"github.com/gogo/protobuf/proto"
tmbytes "github.com/lazyledger/lazyledger-core/libs/bytes"
tmproto "github.com/lazyledger/lazyledger-core/proto/tendermint/types"
"github.com/lazyledger/nmt/namespace"
"github.com/lazyledger/rsmt2d"
)

// Share contains the raw share data without the corresponding namespace.
Expand Down Expand Up @@ -42,7 +46,6 @@ func (tx Tx) MarshalDelimited() ([]byte, error) {
lenBuf := make([]byte, binary.MaxVarintLen64)
length := uint64(len(tx))
n := binary.PutUvarint(lenBuf, length)

return append(lenBuf[:n], tx...), nil
}

Expand Down Expand Up @@ -86,7 +89,7 @@ func splitContiguous(nid namespace.ID, rawDatas [][]byte) []NamespacedShare {
startIndex := 0
rawData, outerIndex, innerIndex, startIndex = getNextChunk(rawDatas, outerIndex, innerIndex, TxShareSize)
rawShare := append(append(append(
make([]byte, 0, len(nid)+1+len(rawData)),
make([]byte, 0, ShareSize),
nid...),
byte(startIndex)),
evan-forbes marked this conversation as resolved.
Show resolved Hide resolved
rawData...)
Expand All @@ -97,12 +100,10 @@ func splitContiguous(nid namespace.ID, rawDatas [][]byte) []NamespacedShare {
return shares
}

// TODO(ismail): implement corresponding merge method for clients requesting
// shares for a particular namespace
func split(rawData []byte, nid namespace.ID) []NamespacedShare {
shares := make([]NamespacedShare, 0)
firstRawShare := append(append(
make([]byte, 0, len(nid)+len(rawData[:MsgShareSize])),
make([]byte, 0, ShareSize),
nid...),
rawData[:MsgShareSize]...,
)
Expand All @@ -111,7 +112,7 @@ func split(rawData []byte, nid namespace.ID) []NamespacedShare {
for len(rawData) > 0 {
shareSizeOrLen := min(MsgShareSize, len(rawData))
rawShare := append(append(
make([]byte, 0, len(nid)+1+len(rawData[:shareSizeOrLen])),
make([]byte, 0, ShareSize),
nid...),
rawData[:shareSizeOrLen]...,
)
Expand Down Expand Up @@ -184,3 +185,321 @@ func zeroPadIfNecessary(share []byte, width int) []byte {
}
return share
}

// DataFromSquare extracts block data from an extended data square.
func DataFromSquare(eds *rsmt2d.ExtendedDataSquare) (Data, error) {
liamsi marked this conversation as resolved.
Show resolved Hide resolved
originalWidth := eds.Width() / 2

// sort block data by namespace
// define a slice for the raw share data of each type
var (
// transactions
txsShares [][]byte
// intermediate state roots
isrShares [][]byte
// evidence
evdShares [][]byte
// messages
msgShares [][]byte
liamsi marked this conversation as resolved.
Show resolved Hide resolved
)

// iterate over each row index
for x := uint(0); x < originalWidth; x++ {
// iterate over each col index
for y := uint(0); y < originalWidth; y++ {
// sort the data of that share types via namespace
share := eds.Cell(x, y)
nid := share[:NamespaceSize]
switch {
case bytes.Equal(TxNamespaceID, nid):
txsShares = append(txsShares, share)

case bytes.Equal(IntermediateStateRootsNamespaceID, nid):
isrShares = append(isrShares, share)

case bytes.Equal(EvidenceNamespaceID, nid):
evdShares = append(evdShares, share)

case bytes.Equal(TailPaddingNamespaceID, nid):
continue

// ignore unused but reserved namespaces
case bytes.Compare(nid, MaxReservedNamespace) < 1:
continue

// every other namespaceID should be a message
default:
msgShares = append(msgShares, share)
}
Copy link
Member

@adlerjohn adlerjohn Apr 1, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic doesn't seem to conform to specs. Potential namespace IDs: https://github.com/lazyledger/lazyledger-specs/blob/de5f4f74f56922e9fa735ef79d9e6e6492a2bad1/specs/consensus.md#reserved-namespace-ids. Namespaces up to and including NAMESPACE_ID_MAX_RESERVED = 255 are reserved, not messages: https://github.com/lazyledger/lazyledger-specs/blob/de5f4f74f56922e9fa735ef79d9e6e6492a2bad1/specs/consensus.md#constants.

There should also be a check somewhere that a reserved namespace ID that's up to 255 but not specifically reserved is invalid.

Copy link
Member Author

@evan-forbes evan-forbes Apr 1, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for pointing this out! added issue #67 in the lazyledger-app to add a check in MsgWirePayForMessage's ValidateBasic method, which get's called with every call to CheckTx.

Added a case for this along with a namespace var in consts.go to represent the max namespaceID https://github.com/lazyledger/lazyledger-core/blob/d118716769036925491d0412772693a55546ca2c/types/shares.go#L227

Should I also add the const NamespaceIDMaxReserved and use that to construct MaxReservedNamespaceID?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should I also add the const NamespaceIDMaxReserved and use that to construct MaxReservedNamespaceID?

Nah, the setup you have now should be sufficient I think.

Copy link
Member

@adlerjohn adlerjohn Apr 2, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One additional check that needs to be somewhere (probably in a future PR so as not to overload this one) is that padding shares between a transaction and a message must have namespace ID TAIL_TRANSACTION_PADDING_NAMESPACE_ID (all the way at the bottom of https://github.com/lazyledger/lazyledger-specs/blob/de5f4f74f56922e9fa735ef79d9e6e6492a2bad1/specs/data_structures.md#arranging-available-data-into-shares, under the giant figures).

The non-interactive default rules may introduce empty shares that do not belong to any message (in the example above, the top-right share is empty). These are zeroes with namespace ID equal to the either TAIL_TRANSACTION_PADDING_NAMESPACE_ID if between a request with a reserved namespace ID and a message, or the namespace ID of the previous message if succeeded by a message. See the rationale doc for more info.

}
}

// pass the raw share data to their respective parsers
txs, err := parseTxs(txsShares)
if err != nil {
return Data{}, err
}

isrs, err := parseISRs(isrShares)
if err != nil {
return Data{}, err
}

evd, err := parseEvd(evdShares)
if err != nil {
return Data{}, err
}

msgs, err := parseMsgs(msgShares)
if err != nil {
return Data{}, err
}

return Data{
Txs: txs,
IntermediateStateRoots: isrs,
Evidence: evd,
Messages: msgs,
}, nil
}

// parseTxs collects all of the transactions from the shares provided
func parseTxs(shares [][]byte) (Txs, error) {
// parse the sharse
rawTxs, err := processContiguousShares(shares)
if err != nil {
return nil, err
}

// convert to the Tx type
txs := make(Txs, len(rawTxs))
for i := 0; i < len(txs); i++ {
txs[i] = Tx(rawTxs[i])
}

return txs, nil
}

// parseISRs collects all the intermediate state roots from the shares provided
func parseISRs(shares [][]byte) (IntermediateStateRoots, error) {
rawISRs, err := processContiguousShares(shares)
if err != nil {
return IntermediateStateRoots{}, err
}

ISRs := make([]tmbytes.HexBytes, len(rawISRs))
for i := 0; i < len(ISRs); i++ {
ISRs[i] = rawISRs[i]
}

return IntermediateStateRoots{RawRootsList: ISRs}, nil
}

// parseEvd collects all evidence from the shares provided.
func parseEvd(shares [][]byte) (EvidenceData, error) {
// the raw data returned does not have length delimiters or namespaces and
// is ready to be unmarshaled
rawEvd, err := processContiguousShares(shares)
if err != nil {
return EvidenceData{}, err
}

evdList := make(EvidenceList, len(rawEvd))

// parse into protobuf bytes
for i := 0; i < len(rawEvd); i++ {
// unmarshal the evidence
var protoEvd *tmproto.Evidence
err := proto.Unmarshal(rawEvd[i], protoEvd)
liamsi marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return EvidenceData{}, err
}

evd, err := EvidenceFromProto(protoEvd)
if err != nil {
return EvidenceData{}, err
}

evdList[i] = evd
}

return EvidenceData{Evidence: evdList}, nil
}

// parseMsgs collects all messages from the shares provided
func parseMsgs(shares [][]byte) (Messages, error) {
msgList, err := parseMsgShares(shares)
if err != nil {
return MessagesEmpty, err
}

return Messages{
MessagesList: msgList,
}, nil
}

// processContiguousShares takes raw shares and extracts out transactions,
// intermediate state roots, or evidence. The returned [][]byte do have
// namespaces or length delimiters and are ready to be unmarshalled
func processContiguousShares(shares [][]byte) (txs [][]byte, err error) {
if len(shares) == 0 {
return nil, nil
}
share := shares[0][NamespaceSize+ShareReservedBytes:]
evan-forbes marked this conversation as resolved.
Show resolved Hide resolved
share, txLen, err := parseDelimiter(share)
if err != nil {
return nil, err
}

for i := 0; i < len(shares); i++ {
var newTxs [][]byte
newTxs, share, txLen, err = collectTxsFromShare(share, txLen)
if err != nil {
return nil, err
}

// add the collected txs to the output
txs = append(txs, newTxs...)

// if there is no next share, the work is done
// if not, then we know there are more shares to process
if len(shares) == i+1 {
break
}

nextShare := shares[i+1][NamespaceSize+ShareReservedBytes:]

// if there is no current share remaining, process the next share
if len(share) == 0 {
share, txLen, err = parseDelimiter(nextShare)
if err != nil {
break
}
continue
}

// create the next share by merging the next share with the remaining of
// the current share
share = append(share, nextShare...)
}

return txs, err
}

func collectTxsFromShare(share []byte, txLen uint64) (txs [][]byte, extra []byte, l uint64, err error) {
for uint64(len(share)) >= txLen {
tx := share[:txLen]
if len(tx) == 0 {
share = nil
break
}

txs = append(txs, tx)
share = share[txLen:]

share, txLen, err = parseDelimiter(share)
if txLen == 0 || err != nil {
break
}
}
return txs, share, txLen, err
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO, this can serve as a kind of blueprint for the pseudo-code that could end up in the spec.


// parseMessages iterates through raw shares and separates the contiguous chunks
// of data. we use this for transactions, evidence, and intermediate state roots
func parseMsgShares(shares [][]byte) ([]Message, error) {
evan-forbes marked this conversation as resolved.
Show resolved Hide resolved
if len(shares) == 0 {
return nil, nil
}

// set the first nid and current share
nid := shares[0][:NamespaceSize]
currentShare := shares[0][NamespaceSize:]

// find and remove the msg len delimiter
currentShare, msgLen, err := parseDelimiter(currentShare)
if err != nil {
return nil, err
}

var msgs []Message
for cursor := uint64(0); cursor < uint64(len(shares)); {
var msg Message
currentShare, nid, cursor, msgLen, msg, err = nextMsg(
shares,
currentShare,
nid,
cursor,
msgLen,
)
if err != nil {
return nil, err
}
if msg.Data != nil {
msgs = append(msgs, msg)
}
}

return msgs, nil
}

func nextMsg(
shares [][]byte,
current,
nid []byte,
cursor,
msgLen uint64,
) ([]byte, []byte, uint64, uint64, Message, error) {
switch {
// the message uses all of the current share data and at least some of the
// next share
case msgLen > uint64(len(current)):
// add the next share to the current one and try again
cursor++
current = append(current, shares[cursor][NamespaceSize:]...)
return nextMsg(shares, current, nid, cursor, msgLen)

// the msg we're looking for is contained in the current share
case msgLen <= uint64(len(current)):
msg := Message{nid, current[:msgLen]}
cursor++

// call it a day if the work is done
if cursor >= uint64(len(shares)) {
return nil, nil, cursor, 0, msg, nil
}

nextNid := shares[cursor][:NamespaceSize]
next, msgLen, err := parseDelimiter(shares[cursor][NamespaceSize:])
return next, nextNid, cursor, msgLen, msg, err
}
// this code is unreachable but the compiler doesn't know that
return nil, nil, 0, 0, MessageEmpty, nil
}

// parseDelimiter finds and returns the length delimiter of the message provided
// while also removing the delimiter bytes from the input
func parseDelimiter(input []byte) ([]byte, uint64, error) {
if len(input) == 0 {
return input, 0, nil
}

l := binary.MaxVarintLen64
if len(input) < binary.MaxVarintLen64 {
l = len(input)
}

// read the length of the message
r := bytes.NewBuffer(input[:l])
msgLen, err := binary.ReadUvarint(r)
if err != nil {
return nil, 0, err
}

// calculate the number of bytes used by the delimiter
lenBuf := make([]byte, binary.MaxVarintLen64)
n := binary.PutUvarint(lenBuf, msgLen)

// return the input without the length delimiter
return input[n:], msgLen, nil
}
Loading