-
Notifications
You must be signed in to change notification settings - Fork 1.1k
PeerDAS: Implement core. #15192
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
PeerDAS: Implement core. #15192
Conversation
bd44664
to
a0d0b93
Compare
consensus-types/blocks/kzg.go
Outdated
@@ -80,6 +80,37 @@ func MerkleProofKZGCommitment(body interfaces.ReadOnlyBeaconBlockBody, index int | |||
return proof, nil | |||
} | |||
|
|||
// MerkleProofKZGCommitments constructs a Merkle proof of inclusion of the KZG | |||
// commitments into the Beacon Block with the given `body` | |||
// TODO: Add missing tests. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add missing tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added in 43ecbda.
690c1ed
to
5207750
Compare
} | ||
|
||
// Returns a serialized random field element in big-endian | ||
func getRandFieldElement(seed int64) [32]byte { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are two identical implementations, could we reuse?
Line 220 in 774b9a7
func GetRandFieldElement(seed int64) [32]byte { |
func getRandFieldElement(seed int64) [32]byte { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is explained in the corresponding commit message: 972a7a6
Aka, for this test only code, duplicating, while not ideal, is, IMO, better than trying to solve the cyclical dependencies.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm I think we might want to fix this dependency. We should be able to import testing/util
anywhere so there is a bad design issue outside of this PR. Someone else will probably run into a similar problem. It is not clear to me what the exact cause is. We can postpone this one.
compilepkg: dependency cycle detected between "github.com/OffchainLabs/prysm/v6/beacon-chain/blockchain/kzg" and "github.com/OffchainLabs/prysm/v6/testing/util" in file "/private/var/tmp/_bazel_t/3a219d5e47df5947c1056b3e139abb7c/sandbox/darwin-sandbox/627/execroot/_main/beacon-chain/blockchain/kzg/validation_test.go"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, I created this ticket: #15231 for book keeping.
} | ||
|
||
// Returns a random blob using the passed seed as entropy | ||
func getRandBlob(seed int64) GoKZG.Blob { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same, the implementations are duplicated in deneb.go
and util_test.go
. We should find a way to consolidate them to avoid copy-pasting each time
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is explained in the corresponding commit message: 972a7a6
Aka, for this test only code, duplicating, while not ideal, is, IMO, better than trying to solve the cyclical dependencies.
} | ||
|
||
sidecar := ðpb.DataColumnSidecar{ | ||
Index: columnIndex, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Need a rebase to fix these errors
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As explained in the PR description, we need #15187 to be merged before rebasing. This dependent PR not be merged is the main reason why this PR is still in draft.
|
||
var ( | ||
// Custom errors | ||
ErrCustodyGroupTooLarge = errors.New("custody group too large") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ErrCustodyGroupTooLarge
could be private, and maybe exported later
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This error in the peerdas_test
package. As a consequence, we need to export it else the build is broken.
// Convert to little endian. | ||
currentIdBytesLittleEndian := bytesutil.ReverseByteOrder(currentIdBytesBigEndian[:]) | ||
|
||
// Hash the result. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a personal preference and a minor nit — I don't think we need comments for code that's already self-explanatory, like hash.Hash
if currentId.Cmp(maxUint256) == 0 { | ||
currentId = uint256.NewInt(0) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This has a bug where currentId = 0
and currentId.Add(currentId, one)
can occur in the same loop iteration due to the for loop’s post-expression. This causes the currentId == 0
case to be skipped entirely.
Spec:
if current_id == UINT256_MAX:
# Overflow prevention
current_id = uint256(0)
else:
current_id += 1
I'd implement it like this:
custodyGroups := make(map[uint64]bool, custodyGroupCount)
currentID := new(uint256.Int).SetBytes(nodeID.Bytes())
one := uint256.NewInt(1)
for uint64(len(custodyGroups)) < custodyGroupCount {
input := bytesutil.ReverseByteOrder(currentID.Bytes32()[:])
hashBytes := hash.Hash(input)
groupID := binary.LittleEndian.Uint64(hashBytes[:8]) % totalGroups
custodyGroups[groupID] = true
if currentID.Cmp(maxUint256) == 0 {
currentID = uint256.NewInt(0)
} else {
currentID.Add(currentID, one)
}
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
😱 Nice catch, thanks!
This bug is even not caught by spec tests, because all spec tests implying the maximum node ID (or close to):
get_custody_groups__max_node_id_max_custody_group_count
get_custody_groups__max_node_id_max_custody_group_count_minus_1
get_custody_groups__max_node_id_min_custody_group_count
either imply 128 custody group counts or 0.
For the test with 0 cgc, we trivially get the expected result.
For tests with 128 cgc, we get the expected result, because the only possible solution is all possible groups.
==> Asked to modify spec tests to have at least one test with max node id but custody group count nor min neither max.
Fixed in fc772d5.
Also, added a shortcut if custodyGroupCount == params.BeaconConfig().NumberOfCustodyGroups - 1
, since in this case there is no other solution than using all possible groups (done in c5dbcc9.)
return nil, errWrongComputedCustodyGroupCount | ||
} | ||
|
||
return custodyGroups, nil |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The spec returns a sorted list, but this implementation doesn't. It’s likely not consensus-breaking since we're converting from a map, but worth calling out
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in ec56d54.
columns = append(columns, column) | ||
} | ||
|
||
return columns, nil |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
spec sorts columns at the end but I dont see we need to do this as the for loop already sorts it by induction
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree, that's the reason why I did not sort them.
// https://github.com/ethereum/consensus-specs/blob/v1.5.0-beta.3/specs/fulu/das-core.md#get_data_column_sidecars | ||
func DataColumnSidecars(signedBlock interfaces.ReadOnlySignedBeaconBlock, cellsAndProofs []kzg.CellsAndProofs) ([]*ethpb.DataColumnSidecar, error) { | ||
start := time.Now() | ||
if signedBlock == nil || len(cellsAndProofs) == 0 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd recommend signedBlock.IsNil()
for nil check
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 346e7c5.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually we need to do both, as done in other parts of the code base.
signedBlock.IsNil()
will panic if signedBlock == nil
.
blobsCount := len(cellsAndProofs) | ||
sidecars := make([]*ethpb.DataColumnSidecar, 0, numberOfColumns) | ||
for columnIndex := range numberOfColumns { | ||
column := make([]kzg.Cell, 0, blobsCount) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you can preallocate to blobsCount
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hum it is already done thanks to the third argument in
make([]kzg.Cell, 0, blobsCount)
right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see. I was thinking about setting the length and assigning values directly by index. I thought it might be slightly faster since there is no append bookkeeping and it avoids accidental appends beyond the intended number. But the difference is extremely small so using make([]T, 0, n) with append is perfectly fine.
kzgProofOfColumn = append(kzgProofOfColumn, kzgProof) | ||
} | ||
|
||
columnBytes := make([][]byte, 0, blobsCount) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you can pre allocate here and below as well
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same, the pre allocation is done thanks to the third allocation of
columnBytes := make([][]byte, 0, blobsCount)
right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
third value is the capacity, the second is the allocation I think. instead of having append .
can't you do something like
columnBytes := make([][]byte, blobsCount) // len == cap
for i, cell := range column {
columnBytes[i] = cell[:] // slice header only – no copy
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See #15192 (comment) and #15192 (comment).
KzgCommitmentsInclusionProof: kzgCommitmentsInclusionProof, | ||
} | ||
|
||
sidecars = append(sidecars, sidecar) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sidecars
array can also be pre allocated as well
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sidecar is already pre allocated with
sidecars := make([]*ethpb.DataColumnSidecar, 0, numberOfColumns)
(third argument)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same with the other comment can't you do.
sidecars := make([]*ethpb.DataColumnSidecar, numberOfColumns)
and then
sidecar[I] = sidecar
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See #15192 (comment) and #15192 (comment).
// `dataColumnsSidecar` needs to contain the datacolumns corresponding to the non-extended matrix, | ||
// else an error will be returned. | ||
// (`dataColumnsSidecar` can contain extra columns, but they will be ignored.) | ||
func Blobs(indices map[uint64]bool, dataColumnsSidecar []*ethpb.DataColumnSidecar) ([]*blocks.VerifiedROBlob, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How will this function be used?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This can be seen in this PR: #14129.
It is only used in the /eth/v1/beacon/blob_sidecars/{block_id} beacon API handler, where we need to convert data column sidecars into blob sidecars.
} | ||
|
||
// Filter blobs index higher than the blob count. | ||
filteredIndices := make(map[uint64]bool, len(indices)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
couldn't you do this in one for loop and without the extra allocations for filtered indices. Example:
indicesSlice := make([]uint64, 0, len(indices))
for i := range indices {
if i < blobCount {
indicesSlice = append(indicesSlice, i)
}
}
slices.Sort(indicesSlice)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 059b33e.
// If the value is already in the cache, return it. | ||
if value, ok := nodeInfoCache.Get(key); ok { | ||
peerInfo, ok := value.(*info) | ||
if !ok { | ||
return nil, false, errors.New("failed to cast peer info (should never happen)") | ||
} | ||
|
||
return peerInfo, true, nil |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Disclaimer: I'm pessimistic about caching, it's easy to get wrong and hard to debug. The real question is whether caching is necessary here. What's the bottleneck that justifies it? It doesn't hurt to merge without a cache first, monitor live performance, identify real bottlenecks, and then design a cache around that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The real question is whether caching is necessary here
I think, yes.
We particularly want to avoid to re-run again an again this function, which is quite heavy for 2 reasons:
It uses a lot of hashes in a while loop:
...
while len(custody_groups) < custody_group_count:
custody_group = CustodyIndex(
bytes_to_uint64(hash(uint_to_bytes(current_id))[0:8])
% NUMBER_OF_CUSTODY_GROUPS
)
...
And this loop is pure brute force until we like the result because of the if
condition. It can be quite expensive for high custody_group_count
:
...
while len(custody_groups) < custody_group_count:
...
if custody_group not in custody_groups:
custody_groups.append(custody_group)
...
Note: I implemented a hack for the case custody_group_count == NUMBER_OF_CUSTODY_GROUPS
, so it is now very cheap to compute. But the case custody_group_count == NUMBER_OF_CUSTODY_GROUPS - 1
is still very expensive to compute.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, two additional points:
- The used cache (
lru_cache
from Hashicorp) is well tested and already (heavily) used in the Prysm codebase. func Info(nodeID enode.ID, custodyGroupCount uint64)
is a pure mathematical function: It will always return the same result for the same inputs: No side effects. So, IMO, there is no fear to have using a cache in this case.
Or maybe am I missing something?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nah, I just don't like caches because they make bugs easy, but your explanation makes sense. I rest my case
|
||
// VerifyDataColumnSidecar verifies if the data column sidecar is valid. | ||
// https://github.com/ethereum/consensus-specs/blob/v1.5.0-beta.5/specs/fulu/p2p-interface.md#verify_data_column_sidecar | ||
func VerifyDataColumnSidecar(sidecar blocks.RODataColumn) error { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We would typically have this under the verification
package
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The content of the peerdas
packages is consistent with the spec.
For example, functions in https://github.com/ethereum/consensus-specs/blob/v1.5.0-beta.5/specs/fulu/p2p-interface.md are implemented in peerdas/p2p_interface.go
.
But, indeed, this function (VerifyDataColumnSidecar
) is used in the verification
package. (To be merged later.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ya, I think we typically have p2p-interface
validation logic in verification
package or in sync
directly but dont feel strongly here
CustodyGroups map[uint64]bool | ||
CustodyColumns map[uint64]bool | ||
DataColumnsSubnets map[uint64]bool |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Two open questions
- Do we really need all three sets? At a minimum, custody groups or columns might be sufficient since everything else can be derived from them
- Is a map the right structure for this, or would a list or sorted uint64 work better?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
-
DataColumnsSubnets
can be recomputed fromCustodyColumns
, andCustodyColumns
can be recomputed fromCustodyGroups
. But as said here, computingCustodyGroups
is quite heavy. And also, computingCustodyColumns
fromCustodyGroups
andDataColumnsSubnets
fromCustodyColumns
is done with pure mathematical functions. So, once we haveCustodyGroups
cached, it's quite convenient to cache the 2 others, to avoid recomputing them again and again in multiple locations in the codebase. -
The advantage of using an
uint64
set (which is represented by amap[uint64]bool
in go since there is no builtin set), is to be able to know if a value is in this set in anO(1)
fashion. For a sorted list, is isO(log(n))
. (So up to 7x more operations for128
items.) IMO, using a set is strictly better in our case, since we often need to look if a given value is in the struct.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, but I think although map gives O(1) lookup, it is not free in practice because of memory overhead and GC pressure compared to slices. Lookup time for small n is likely negligible, and []uint64
is a flat contiguous block of memory. I think maps are optimized for unknown or large-scale sets, and for small predictable sets, slices might be better
Don't feel strongly about this one, just my 2c
commitments := make([]kzg.Bytes48, 0, count) | ||
indices := make([]uint64, 0, count) | ||
cells := make([]kzg.Cell, 0, count) | ||
proofs := make([]kzg.Bytes48, 0, count) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can just pre-allocate the slice here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They are actually pre allocated thanks to the third argument.
Extract of the go doc for make
:
The make built-in function allocates and initializes an object of type slice, map, or chan (only). Like new, the first argument is a type, not a value. Unlike new, make's return type is the same as the type of its argument, not a pointer to it. The specification of the result depends on the type:
- Slice: The size specifies the length. The capacity of the slice is equal to its length. A second integer argument may be provided to specify a different capacity; it must be no smaller than the length. For example, make([]int, 0, 10) allocates an underlying array of size 10 and returns a slice of length 0 and capacity 10 that is backed by this underlying array.
- ...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ya I think you are right here, it's my misunderstanding, please ignore all the relevant feedback about pre alloctaions
columnsPerGroup := numberOfColumns / numberOfCustodyGroups | ||
return columnIndex / columnsPerGroup, nil |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This implementation is not consistent with ComputeColumnsForCustodyGroup
which uses
column := numberOfCustodyGroup*i + custodyGroup
That means column c belongs to custody group c % numberOfCustodyGroups
So to compute the custody group for a column index, the logic should be:
return columnIndex % numberOfCustodyGroups, nil
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
😱 Nice catch, thanks!
Actually unit tests are OK as well, since my logic was both wrong for the function itself and the tests...
(Note this bug luckily does not currently impact the devnet, since there is 128 groups and 128 columns, so columnIndex / columnsPerGroup == columnIndex % numberOfCustodyGroups == columnIndex
.)
Fixed in abe4831.
"github.com/OffchainLabs/prysm/v6/config/params" | ||
) | ||
|
||
// ExtendedSampleCount computes, for a given number of samples per slot and allowed failures the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we wait to merge these and focus on custody sampling first?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The whole peer sampling code is currently not used at all.
However, it is already implemented and well unit tested, so there is no harm to merge it as is.
Note I'm not strongly against not to merge it right now. (No strong opinion about it.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not against merging it, but I probably won't review it here because this PR is already big enough and already contains a lot of critical logic to review. We can either merge it to develop and hope someone who uses it later will review it, or merge it later in another peerDAS
to develop
PR. Up to you!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed in 2543120.
Peer sampling is not lost, since still living in the peerDAS
branch.
For now, let's not merge it into develop
and postpone the decision to later.
beacon-chain/core/peerdas/util.go
Outdated
var proofs []kzg.Proof | ||
for idx := i * numColumns; idx < (i+1)*numColumns; idx++ { | ||
proofs = append(proofs, kzg.Proof(cellProofs[idx])) | ||
} | ||
|
||
cellsAndProofs = append(cellsAndProofs, kzg.CellsAndProofs{ | ||
Cells: cells, | ||
Proofs: proofs, | ||
}) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The whole idea of constructCellsAndProofs
and the cellsAndProofs
struct feels a bit extra to me. We could just compute the cells directly in ConstructDataColumnSidecars
and pass (block, cells, cellProofs)
into DataColumnSidecars
, which can handle everything internally. Is there any benefit to keeping the cells and proofs in their own struct again?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For the cellsAndProofs
struct, it is at least needed if we want to stick to the spec for get_data_column_sidecars.
The Python spec uses a single cells_and_kzg_proofs
argument which is a list of tuples of cells and proofs.
Tuples do not exist in go. The closer we can do is the CellsAndProofs
struct.
Now this CellsAndProofs
is defined to stick to the spec, IMO it's better to re-use it in other locations in the codebase rather than using []Cell
and []Proof
separately.
Removed the constructCellsAndProofs
function in 34236e2.
// VerifyDataColumnSidecarInclusionProof verifies if the given KZG commitments included in the given beacon block. | ||
// https://github.com/ethereum/consensus-specs/blob/v1.5.0-beta.5/specs/fulu/p2p-interface.md#verify_data_column_sidecar_inclusion_proof | ||
func VerifyDataColumnSidecarInclusionProof(sidecar blocks.RODataColumn) error { | ||
if sidecar.SignedBlockHeader == nil || sidecar.SignedBlockHeader.Header == nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do you not need to verify the sidecar itself? i think we've used IsEmpty() function in the past for these
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is no IsEmpty
(or IsNil
) function defined for blocks.RODataColumn
).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do you think there should be?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hum not sure.
A IsNil
function would check if every field is non nil right?
Here we just need the Header
(and all its parents) to be non nil.
func leavesFromCommitments(commitments [][]byte) [][]byte { | ||
// MerkleProofKZGCommitments constructs a Merkle proof of inclusion of the KZG | ||
// commitments into the Beacon Block with the given `body` | ||
func MerkleProofKZGCommitments(body interfaces.ReadOnlyBeaconBlockBody) ([][]byte, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i'm surprised we didn't have something like this for deneb is this new?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For Deneb we had MerkleProofKZGCommitment
(without the terminal s
) defined. This function is defined just before.
|
||
// ConstructDataColumnSidecars constructs data column sidecars from a block, blobs and their cell proofs. | ||
// This is a convenience method as blob and cell proofs are common inputs. | ||
func ConstructDataColumnSidecars(block interfaces.SignedBeaconBlock, blobs [][]byte, cellProofs [][]byte) ([]*ethpb.DataColumnSidecar, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this function is used anywhere, should we include this right now ( I get it's in core )
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is not. It is used on on the peerDAS branch. Will be merged in a future PR.
return nil, errors.Wrap(err, "blob KZG commitments") | ||
} | ||
|
||
if len(blobKzgCommitments) != len(cellsAndProofs) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is 0 ok here? or do we need to exit early ever? i think if it's 0 here then we still set the metric for datacolumn time
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah appart for the wrong metric it's OK to have 0
.
However:
- as you said the metric is now misleading, and
- we run some functions for nothing: Ex
signedBlock.Header
.
==> Better to exit early if 0.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually I was already checked at the beginning of the function:
if signedBlock == nil || signedBlock.IsNil() || len(cellsAndProofs) == 0 {
return nil, nil
}
but not tested.
I added a test for that (and one extra test for sizes mismatch) in 1d8fb51.
|
||
kzgProofOfColumnBytes := make([][]byte, 0, blobsCount) | ||
for _, kzgProof := range kzgProofOfColumn { | ||
copiedProof := kzgProof |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we need to do this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hum it was more a safety feature (at but at the cost of a copy).
I removed it and added a warning in the godoc, in ea5e0e8.
|
||
// Get missing columns. | ||
if actualColumnCount < neededColumnCount { | ||
missingColumns := make(map[uint64]bool, neededColumnCount-actualColumnCount) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do you actually need this map? you could do
for I :=range neededColumnCount {
if _, ok := sliceIndexFromColumnIndex[i]; !ok {
missing = append(missing, i)
}
}
if len(missing) > 0 {
return nil, fmt.Errorf("some columns are missing: %v", missing)
}
```
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 8dac003.
recoveredCellsAndProofs := make([]kzg.CellsAndProofs, blobCount) | ||
|
||
for blobIndex := 0; blobIndex < blobCount; blobIndex++ { | ||
bIndex := blobIndex |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we need this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See #15192 (comment).
return recoveredCellsAndProofs, nil | ||
} | ||
|
||
// DataColumnSidecarsForReconstruct is a TEMPORARY function until there is an official specification for it. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should we add like a todo on removal? i missed that this is temporary
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in c87701e.
Actually in the (not yet merged) code using the peerDAS
package, we need both to reconstruct data column sidecars from a signed block (as the spec specifies) and from signed block header, blob KZG commitments and KZG commitment inclusion proofs, which can be extracted from the signed block.
==> The heavy duty is now done by DataColumnsSidecarsFromItems
, which is called by DataColumnSidecars
.
|
||
for i, blob := range blobs { | ||
var b kzg.Blob | ||
copy(b[:], blob) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we actually need to do this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, because b
is of kzg.Blob
type, and blob is of []byte
type.
An other way to do this would be to write:
b := kzg.Blob(blob)
but the later can panic, contrary to the former (and a copy is done in both cases).
func ValidatorsCustodyRequirement(state beaconState.ReadOnlyBeaconState, validatorsIndex map[primitives.ValidatorIndex]bool) (uint64, error) { | ||
totalNodeBalance := uint64(0) | ||
for index := range validatorsIndex { | ||
validator, err := state.ValidatorAtIndex(index) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this should be ValidatorAtIndexReadOnly
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 2d4d8d1.
|
||
// Filter blobs index higher than the blob count. | ||
indicesSlice := make([]uint64, 0, len(indices)) | ||
for i := range indices { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what do we do if indices is less than blobcount? or is that not possible?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry not sure to understand.
Here we want to only keep indices lower than blobCount
.
So if an index is lower than blobCount
, we keep it, else we filter it out.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I meant what do we do if the number of indices keys in the map in the argument is less than the number for blobCount, and if that's possible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nvm i misunderstood, as long as there is 1 its ok
dataColumnSideCar := dataColumnsSidecar[sliceIndex] | ||
cell := dataColumnSideCar.Column[blobIndex] | ||
|
||
for i := range cell { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what if this code was changed to the following?
cell := dataColumnsSidecar[sliceIdx].Column[blobIndex]
if len(cell) != kzg.BytesPerCell {
return nil, fmt.Errorf("column %d: expected %d‑byte cell, got %d",
columnIndex, kzg.BytesPerCell, len(cell))
}
offset := uint64(columnIndex)*uint64(kzg.BytesPerCell)
end := offset + uint64(len(cell))
if end > uint64(kzg.BytesPerBlob) {
return nil, fmt.Errorf("column %d overflows blob (%d > %d)",
columnIndex, end, kzg.BytesPerBlob)
}
copy(blob[offset:end], cell)
```
do you think we need these checks?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we beed these checks, since all values are already trusted, either because recomputed by ourself, either because verified in data column sidecars pipeline if coming from a peer.
func CustodyColumns(custodyGroups []uint64) (map[uint64]bool, error) { | ||
numberOfCustodyGroups := params.BeaconConfig().NumberOfCustodyGroups | ||
|
||
custodyGroupCount := len(custodyGroups) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is this correct? you loop over groupColumns that are calculated and set to true, not the custodyColumns length.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it is correct:
- For each group, we compute columns.
- For each column (belonging to a group), we add it to the
columns
map.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it's not possible for the columns from the groupColumns to be a different number than custodyGroupsCount? i guess it will just add to the map but it would look strange
// RecoverCellsAndProofs recovers the cells and proofs from the data column sidecars. | ||
func RecoverCellsAndProofs( | ||
dataColumnSideCars []*ethpb.DataColumnSidecar, | ||
blockRoot [fieldparams.RootLength]byte, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why is blockRoot not used in this instance?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because we don't need it :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 182b5d7.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's all I was able to comment on, LGTM since some others have reviewed for architecture specific
Approved based on the issues I found and fixed, but feel free to get few more eyes on it. This was a long and critical PR to review, and given the size and constraints, I’m not confident that myself is enough for a thorough, high-confidence review |
We have @nisdas who reviewed it before merging in the peerdas branch, then @terencechain and @james-prysm . I guess we can merge now . |
Please read commit by commit.
Implement the specification: https://github.com/ethereum/consensus-specs/tree/dev/specs/fulu
What type of PR is this?
Feature
Other notes for review
Depends on:
kzg
package. #15186Acknowledgements