Files
prysm/encoding/ssz/query/query_test.go
fernantho 0476eeda57 SSZ-QL: custom Generic Merkle Proofs building the tree and collecting the hashes in one sweep (#16177)
<!-- Thanks for sending a PR! Before submitting:

1. If this is your first PR, check out our contribution guide here
https://docs.prylabs.network/docs/contribute/contribution-guidelines
You will then need to sign our Contributor License Agreement (CLA),
which will show up as a comment from a bot in this pull request after
you open it. We cannot review code without a signed CLA.
2. Please file an associated tracking issue if this pull request is
non-trivial and requires context for our team to understand. All
features and most bug fixes should have
an associated issue with a design discussed and decided upon. Small bug
   fixes and documentation improvements don't need issues.
3. New features and bug fixes must have tests. Documentation may need to
be updated. If you're unsure what to update, send the PR, and we'll
discuss
   in review.
4. Note that PRs updating dependencies and new Go versions are not
accepted.
   Please file an issue instead.
5. A changelog entry is required for user facing issues.
-->

**What type of PR is this?**
Feature

**What does this PR do? Why is it needed?**
This PR replaces the previous PR
https://github.com/OffchainLabs/prysm/pull/16121, which built the entire
Merkle tree and generated proofs only after the tree was complete. In
this PR, the Merkle proof is produced by collecting hashes while the
Merkle tree is being built. This approach has proven to be more
efficient than the one in
https://github.com/OffchainLabs/prysm/pull/16121.

- **ProofCollector**: 
- New `ProofCollector` type in `encoding/ssz/query/proof_collector.go`:
Collects sibling hashes and leaves needed for Merkle proofs during
merkleization.
- Multiproof-ready design with `requiredSiblings`/`requiredLeaves` maps
for registering target gindices before merkleization.
- Thread-safe: read-only required maps during merkleization,
mutex-protected writes to `siblings`/`leaves`.
- `AddTarget(gindex)` registers a target leaf and computes all required
sibling gindices along the path to root.
- `toProof()` converts collected data into `fastssz.Proof` structure.
- Parallel execution in `merkleizeVectorBody` for composite elements
with worker pool pattern.
- Optimized container hashing: Generalized
`stateutil.OptimizedValidatorRoots` pattern for any SSZ container type:
- `optimizedContainerRoots`: Parallelized field root computation +
level-by-level vectorized hashing via `VectorizedSha256`.
- `hashContainerHelper`: Worker goroutine for processing container
subsets.
- `containerFieldRoots`: Computes field roots for a single container
using reflection and SszInfo metadata.

- **`Prove(gindex)` method** in `encoding/ssz/query/merkle_proof.go`:
Entry point for generating SSZ Merkle proofs for a given generalized
index.

- **Testing**
- Added `merkle_proof_test.go` and `proof_collector_test.go` to test and
benchmark this feature.

The main outcomes of the optimizations are here:
```
❯ go test ./encoding/ssz/query -run=^$ -bench='Benchmark(OptimizedContainerRoots|OptimizedValidatorRoots|ProofCollectorMerkleize)$' -benchmem
goos: darwin
goarch: arm64
pkg: github.com/OffchainLabs/prysm/v7/encoding/ssz/query
cpu: Apple M2 Pro
BenchmarkOptimizedValidatorRoots-10         3237            361029 ns/op          956858 B/op       6024 allocs/op
BenchmarkOptimizedContainerRoots-10         1138            969002 ns/op         3245223 B/op      11024 allocs/op
BenchmarkProofCollectorMerkleize-10          522           2262066 ns/op         3216000 B/op      19000 allocs/op
PASS
ok      github.com/OffchainLabs/prysm/v7/encoding/ssz/query     4.619s
```
Knowing that `OptimizedValidatorRoots` implements very effective
optimizations, `OptimizedContainerRoots` mimics them.
In the benchmark we can see that `OptimizedValidatorRoots` remain as the
most performant and tit the baseline here:
- `ProofCollectorMerkleize` is **~6.3× slower**, uses **~3.4× more
memory** (B/op), and performs **~3.2× more allocations**.
- `OptimizedContainerRoots` sits in between: it’s **~2.7× slower** than
`OptimizedValidatorRoots` (and **~3.4× higher B/op**, **~1.8× more
allocations**), but it is a clear win over `ProofCollectorMerkleize` for
lists/vectors: **~2.3× faster** with **~1.7× fewer allocations** (and
essentially the same memory footprint).

The main drawback is that `OptimizedContainerRoots` can only be applied
to vector/list subtrees where we don’t need to collect any sibling/leaf
data (i.e., no proof targets within that subtree); integrating it into
the recursive merkleize(...) flow when targets are outside the subtree
is expected to land in a follow-up PR.

**Which issues(s) does this PR fix?**
Partially https://github.com/OffchainLabs/prysm/issues/15598

**Other notes for review**
In this [write-up](https://hackmd.io/@fernantho/BJbZ1xmmbg), I depict
the process to come up with this solution.

Future improvements:
- Defensive check that the gindex is not too big, depicted [here](
https://github.com/OffchainLabs/prysm/pull/16177#discussion_r2671684100).
- Integrate optimizedContainerRoots into the recursive merkleize(...)
flow when proof targets are not within the subtree (skip full traversal
for container lists).
- Add multiproofs.
- Connect `proofCollector` to SSZ-QL endpoints (direct integration of
`proofCollector` for BeaconBlock endpoint and "hybrid" approach for
BeaconState endpoint).

**Acknowledgements**

- [x] I have read
[CONTRIBUTING.md](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md).
- [x] I have included a uniquely named [changelog fragment
file](https://github.com/prysmaticlabs/prysm/blob/develop/CONTRIBUTING.md#maintaining-changelogmd).
- [x] I have added a description with sufficient context for reviewers
to understand this PR.
- [x] I have tested that my changes work as expected and I added a
testing plan to the PR description (if applicable).

---------

Co-authored-by: Radosław Kapka <radoslaw.kapka@gmail.com>
Co-authored-by: Jun Song <87601811+syjn99@users.noreply.github.com>
2026-01-28 13:01:22 +00:00

782 lines
20 KiB
Go

package query_test
import (
"math"
"testing"
"github.com/OffchainLabs/go-bitfield"
"github.com/OffchainLabs/prysm/v7/encoding/ssz/query"
"github.com/OffchainLabs/prysm/v7/encoding/ssz/query/testutil"
sszquerypb "github.com/OffchainLabs/prysm/v7/proto/ssz_query/testing"
"github.com/OffchainLabs/prysm/v7/testing/require"
)
func TestSize(t *testing.T) {
tests := []struct {
name string
obj query.SSZObject
expectedSize uint64
}{
{
name: "FixedTestContainer",
obj: &sszquerypb.FixedTestContainer{},
expectedSize: 565,
},
{
name: "VariableTestContainer",
obj: &sszquerypb.VariableTestContainer{},
expectedSize: 132,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
info, err := query.AnalyzeObject(tt.obj)
require.NoError(t, err)
require.NotNil(t, info)
require.Equal(t, tt.expectedSize, info.Size())
})
}
}
func TestCalculateOffsetAndLength(t *testing.T) {
type testCase struct {
name string
path string
expectedOffset uint64
expectedLength uint64
}
t.Run("FixedTestContainer", func(t *testing.T) {
tests := []testCase{
// Basic integer types
{
name: "field_uint32",
path: ".field_uint32",
expectedOffset: 0,
expectedLength: 4,
},
{
name: "field_uint64",
path: ".field_uint64",
expectedOffset: 4,
expectedLength: 8,
},
// Boolean type
{
name: "field_bool",
path: ".field_bool",
expectedOffset: 12,
expectedLength: 1,
},
// Fixed-size bytes
{
name: "field_bytes32",
path: ".field_bytes32",
expectedOffset: 13,
expectedLength: 32,
},
// Nested container
{
name: "nested container",
path: ".nested",
expectedOffset: 45,
expectedLength: 40,
},
{
name: "nested value1",
path: ".nested.value1",
expectedOffset: 45,
expectedLength: 8,
},
{
name: "nested value2",
path: ".nested.value2",
expectedOffset: 53,
expectedLength: 32,
},
// Vector field
{
name: "vector field",
path: ".vector_field",
expectedOffset: 85,
expectedLength: 192, // 24 * 8 bytes
},
// Accessing an element in the vector
{
name: "vector field (0th element)",
path: ".vector_field[0]",
expectedOffset: 85,
expectedLength: 8,
},
{
name: "vector field (10th element)",
path: ".vector_field[10]",
expectedOffset: 165,
expectedLength: 8,
},
// 2D bytes field
{
name: "two_dimension_bytes_field",
path: ".two_dimension_bytes_field",
expectedOffset: 277,
expectedLength: 160, // 5 * 32 bytes
},
// Accessing an element in the 2D bytes field
{
name: "two_dimension_bytes_field (1st element)",
path: ".two_dimension_bytes_field[1]",
expectedOffset: 309,
expectedLength: 32,
},
// Bitvector fields
{
name: "bitvector64_field",
path: ".bitvector64_field",
expectedOffset: 437,
expectedLength: 8,
},
{
name: "bitvector512_field",
path: ".bitvector512_field",
expectedOffset: 445,
expectedLength: 64,
},
// Trailing field
{
name: "trailing_field",
path: ".trailing_field",
expectedOffset: 509,
expectedLength: 56,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
path, err := query.ParsePath(tt.path)
require.NoError(t, err)
info, err := query.AnalyzeObject(&sszquerypb.FixedTestContainer{})
require.NoError(t, err)
_, offset, length, err := query.CalculateOffsetAndLength(info, path)
require.NoError(t, err)
require.Equal(t, tt.expectedOffset, offset, "Expected offset to be %d", tt.expectedOffset)
require.Equal(t, tt.expectedLength, length, "Expected length to be %d", tt.expectedLength)
})
}
})
t.Run("VariableTestContainer", func(t *testing.T) {
tests := []testCase{
// Fixed leading field
{
name: "leading_field",
path: ".leading_field",
expectedOffset: 0,
expectedLength: 32,
},
// Variable-size list fields
{
name: "field_list_uint64",
path: ".field_list_uint64",
expectedOffset: 116, // First part of variable-sized type.
expectedLength: 40, // 5 elements * uint64 (8 bytes each)
},
// Accessing an element in the list
{
name: "field_list_uint64 (2nd element)",
path: ".field_list_uint64[2]",
expectedOffset: 132,
expectedLength: 8,
},
{
name: "field_list_container",
path: ".field_list_container",
expectedOffset: 156, // Second part of variable-sized type.
expectedLength: 120, // 3 elements * FixedNestedContainer (40 bytes each)
},
// Accessing an element in the list of containers
{
name: "field_list_container (1st element)",
path: ".field_list_container[1]",
expectedOffset: 196,
expectedLength: 40,
},
{
name: "field_list_bytes32",
path: ".field_list_bytes32",
expectedOffset: 276,
expectedLength: 96, // 3 elements * 32 bytes each
},
// Accessing an element in the list of bytes32
{
name: "field_list_bytes32 (0th element)",
path: ".field_list_bytes32[0]",
expectedOffset: 276,
expectedLength: 32,
},
{
name: "field_list_bytes32 (2nd element)",
path: ".field_list_bytes32[2]",
expectedOffset: 340,
expectedLength: 32,
},
// Nested paths
{
name: "nested",
path: ".nested",
expectedOffset: 372,
// Calculated with:
// - Value1: 8 bytes
// - field_list_uint64 offset: 4 bytes
// - field_list_uint64 length: 40 bytes
// - nested_list_field offset: 4 bytes
// - nested_list_field length: 99 bytes
// - 3 offset pointers for each element in nested_list_field: 12 bytes
// Total: 8 + 4 + 40 + 4 + 99 + 12 = 167 bytes
expectedLength: 167,
},
{
name: "nested.value1",
path: ".nested.value1",
expectedOffset: 372,
expectedLength: 8,
},
{
name: "nested.field_list_uint64",
path: ".nested.field_list_uint64",
expectedOffset: 388,
expectedLength: 40,
},
{
name: "nested.field_list_uint64 (3rd element)",
path: ".nested.field_list_uint64[3]",
expectedOffset: 412,
expectedLength: 8,
},
{
name: "nested.nested_list_field",
path: ".nested.nested_list_field",
expectedOffset: 440,
expectedLength: 99,
},
// Accessing an element in the nested list of bytes
{
name: "nested.nested_list_field (1st element)",
path: ".nested.nested_list_field[1]",
expectedOffset: 472,
expectedLength: 33,
},
{
name: "nested.nested_list_field (2nd element)",
path: ".nested.nested_list_field[2]",
expectedOffset: 505,
expectedLength: 34,
},
// Variable list of variable-sized containers
{
name: "variable_container_list",
path: ".variable_container_list",
expectedOffset: 547,
expectedLength: 604,
},
// Bitlist field
{
name: "bitlist_field",
path: ".bitlist_field",
expectedOffset: 1151,
expectedLength: 33, // 32 bytes + 1 byte for length delimiter
},
// 2D bytes field
{
name: "nested_list_field",
path: ".nested_list_field",
expectedOffset: 1196,
expectedLength: 99,
},
// Accessing an element in the list of nested bytes
{
name: "nested_list_field (0th element)",
path: ".nested_list_field[0]",
expectedOffset: 1196,
expectedLength: 32,
},
{
name: "nested_list_field (1st element)",
path: ".nested_list_field[1]",
expectedOffset: 1228,
expectedLength: 33,
},
{
name: "nested_list_field (2nd element)",
path: ".nested_list_field[2]",
expectedOffset: 1261,
expectedLength: 34,
},
// Fixed trailing field
{
name: "trailing_field",
path: ".trailing_field",
expectedOffset: 60, // After leading_field + 7 offset pointers
expectedLength: 56,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
path, err := query.ParsePath(tt.path)
require.NoError(t, err)
testContainer := createVariableTestContainer()
info, err := query.AnalyzeObject(testContainer)
require.NoError(t, err)
_, offset, length, err := query.CalculateOffsetAndLength(info, path)
require.NoError(t, err)
require.Equal(t, tt.expectedOffset, offset, "Expected offset to be %d", tt.expectedOffset)
require.Equal(t, tt.expectedLength, length, "Expected length to be %d", tt.expectedLength)
})
}
})
}
func TestHashTreeRoot(t *testing.T) {
tests := []struct {
name string
obj query.SSZObject
}{
{
name: "FixedNestedContainer",
obj: &sszquerypb.FixedNestedContainer{
Value1: 42,
Value2: []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08},
},
},
{
name: "FixedTestContainer",
obj: createFixedTestContainer(),
},
{
name: "VariableNestedContainer",
obj: &sszquerypb.VariableNestedContainer{
Value1: 84,
FieldListUint64: []uint64{1, 2, 3, 4, 5},
NestedListField: [][]byte{
{0x0a, 0x0b, 0x0c},
{0x1a, 0x1b, 0x1c, 0x1d},
},
},
},
{
name: "VariableTestContainer",
obj: createVariableTestContainer(),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Analyze the object to get its sszInfo
info, err := query.AnalyzeObject(tt.obj)
require.NoError(t, err)
require.NotNil(t, info, "Expected non-nil SSZ info")
// Call HashTreeRoot on the sszInfo and compare results
hashTreeRoot, err := info.HashTreeRoot()
require.NoError(t, err, "HashTreeRoot should not return an error")
expectedHashTreeRoot, err := tt.obj.HashTreeRoot()
require.NoError(t, err, "HashTreeRoot on original object should not return an error")
// Verify the Merkle tree root matches with the SSZ generated HashTreeRoot
require.Equal(t, expectedHashTreeRoot, hashTreeRoot, "HashTreeRoot from sszInfo should match original object's HashTreeRoot")
})
}
}
func TestRoundTripSszInfo(t *testing.T) {
specs := []testutil.TestSpec{
getFixedTestContainerSpec(),
getVariableTestContainerSpec(),
}
for _, spec := range specs {
testutil.RunStructTest(t, spec)
}
}
func createFixedTestContainer() *sszquerypb.FixedTestContainer {
fieldBytes32 := make([]byte, 32)
for i := range fieldBytes32 {
fieldBytes32[i] = byte(i + 24)
}
nestedValue2 := make([]byte, 32)
for i := range nestedValue2 {
nestedValue2[i] = byte(i + 56)
}
bitvector64 := bitfield.NewBitvector64()
for i := range bitvector64 {
bitvector64[i] = 0x42
}
bitvector512 := bitfield.NewBitvector512()
for i := range bitvector512 {
bitvector512[i] = 0x24
}
trailingField := make([]byte, 56)
for i := range trailingField {
trailingField[i] = byte(i + 88)
}
return &sszquerypb.FixedTestContainer{
// Basic types
FieldUint32: math.MaxUint32,
FieldUint64: math.MaxUint64,
FieldBool: true,
// Fixed-size bytes
FieldBytes32: fieldBytes32,
// Nested container
Nested: &sszquerypb.FixedNestedContainer{
Value1: 123,
Value2: nestedValue2,
},
// Vector field
VectorField: []uint64{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24},
// 2D bytes field
TwoDimensionBytesField: [][]byte{
make([]byte, 32),
make([]byte, 32),
make([]byte, 32),
make([]byte, 32),
make([]byte, 32),
},
// Bitvector fields
Bitvector64Field: bitvector64,
Bitvector512Field: bitvector512,
// Trailing field
TrailingField: trailingField,
}
}
func getFixedTestContainerSpec() testutil.TestSpec {
testContainer := createFixedTestContainer()
return testutil.TestSpec{
Name: "FixedTestContainer",
Type: &sszquerypb.FixedTestContainer{},
Instance: testContainer,
PathTests: []testutil.PathTest{
// Basic types
{
Path: ".field_uint32",
Expected: testContainer.FieldUint32,
},
{
Path: ".field_uint64",
Expected: testContainer.FieldUint64,
},
{
Path: ".field_bool",
Expected: testContainer.FieldBool,
},
// Fixed-size bytes
{
Path: ".field_bytes32",
Expected: testContainer.FieldBytes32,
},
// Nested container
{
Path: ".nested",
Expected: testContainer.Nested,
},
{
Path: ".nested.value1",
Expected: testContainer.Nested.Value1,
},
{
Path: ".nested.value2",
Expected: testContainer.Nested.Value2,
},
// Vector field
{
Path: ".vector_field",
Expected: testContainer.VectorField,
},
{
Path: ".vector_field[0]",
Expected: testContainer.VectorField[0],
},
{
Path: ".vector_field[10]",
Expected: testContainer.VectorField[10],
},
// 2D bytes field
{
Path: ".two_dimension_bytes_field",
Expected: testContainer.TwoDimensionBytesField,
},
{
Path: ".two_dimension_bytes_field[0]",
Expected: testContainer.TwoDimensionBytesField[0],
},
{
Path: ".two_dimension_bytes_field[1]",
Expected: testContainer.TwoDimensionBytesField[1],
},
// Bitvector fields
{
Path: ".bitvector64_field",
Expected: testContainer.Bitvector64Field,
},
{
Path: ".bitvector512_field",
Expected: testContainer.Bitvector512Field,
},
// Trailing field
{
Path: ".trailing_field",
Expected: testContainer.TrailingField,
},
},
}
}
func createVariableTestContainer() *sszquerypb.VariableTestContainer {
leadingField := make([]byte, 32)
for i := range leadingField {
leadingField[i] = byte(i + 100)
}
trailingField := make([]byte, 56)
for i := range trailingField {
trailingField[i] = byte(i + 150)
}
nestedContainers := make([]*sszquerypb.FixedNestedContainer, 3)
for i := range nestedContainers {
value2 := make([]byte, 32)
for j := range value2 {
value2[j] = byte(j + i*32)
}
nestedContainers[i] = &sszquerypb.FixedNestedContainer{
Value1: uint64(1000 + i),
Value2: value2,
}
}
bitlistField := bitfield.NewBitlist(256)
bitlistField.SetBitAt(0, true)
bitlistField.SetBitAt(10, true)
bitlistField.SetBitAt(50, true)
bitlistField.SetBitAt(100, true)
bitlistField.SetBitAt(255, true)
// Total size: 3 lists with lengths 32, 33, and 34 = 99 bytes
nestedListField := make([][]byte, 3)
for i := range nestedListField {
nestedListField[i] = make([]byte, (32 + i)) // Different lengths for each sub-list
for j := range nestedListField[i] {
nestedListField[i][j] = byte(j + i*16)
}
}
// Two VariableOuterContainer elements, each with two VariableInnerContainer elements
variableContainerList := make([]*sszquerypb.VariableOuterContainer, 2)
for i := range variableContainerList {
// Inner1: 8 + 4 + 4 + (8*3) + (4*3) + 99 = 151 bytes
inner1 := &sszquerypb.VariableNestedContainer{
Value1: 42,
FieldListUint64: []uint64{uint64(i), uint64(i + 1), uint64(i + 2)},
NestedListField: nestedListField,
}
// Inner2: 8 + 4 + 4 + (8*2) + (4*3) + 99 = 143 bytes
inner2 := &sszquerypb.VariableNestedContainer{
Value1: 84,
FieldListUint64: []uint64{uint64(i + 3), uint64(i + 4)},
NestedListField: nestedListField,
}
// (4*2) + 151 + 143 = 302 bytes per VariableOuterContainer
variableContainerList[i] = &sszquerypb.VariableOuterContainer{
Inner_1: inner1,
Inner_2: inner2,
}
}
return &sszquerypb.VariableTestContainer{
// Fixed leading field
LeadingField: leadingField,
// Variable-size lists
FieldListUint64: []uint64{100, 200, 300, 400, 500},
FieldListContainer: nestedContainers,
FieldListBytes32: [][]byte{
make([]byte, 32),
make([]byte, 32),
make([]byte, 32),
},
// Variable nested container
Nested: &sszquerypb.VariableNestedContainer{
Value1: 42,
FieldListUint64: []uint64{1, 2, 3, 4, 5},
NestedListField: nestedListField,
},
// Variable list of variable-sized containers
VariableContainerList: variableContainerList,
// Bitlist field
BitlistField: bitlistField,
// 2D bytes field
NestedListField: nestedListField,
// Fixed trailing field
TrailingField: trailingField,
}
}
func getVariableTestContainerSpec() testutil.TestSpec {
testContainer := createVariableTestContainer()
return testutil.TestSpec{
Name: "VariableTestContainer",
Type: &sszquerypb.VariableTestContainer{},
Instance: testContainer,
PathTests: []testutil.PathTest{
// Fixed leading field
{
Path: ".leading_field",
Expected: testContainer.LeadingField,
},
// Variable-size list of uint64
{
Path: ".field_list_uint64",
Expected: testContainer.FieldListUint64,
},
{
Path: ".field_list_uint64[2]",
Expected: testContainer.FieldListUint64[2],
},
// Variable-size list of (fixed-size) containers
{
Path: ".field_list_container",
Expected: testContainer.FieldListContainer,
},
// Accessing an element in the list of containers
{
Path: ".field_list_container[0]",
Expected: testContainer.FieldListContainer[0],
},
{
Path: ".field_list_container[1]",
Expected: testContainer.FieldListContainer[1],
},
// Variable-size list of bytes32
{
Path: ".field_list_bytes32",
Expected: testContainer.FieldListBytes32,
},
// Variable nested container with every path
{
Path: ".nested",
Expected: testContainer.Nested,
},
{
Path: ".nested.value1",
Expected: testContainer.Nested.Value1,
},
{
Path: ".nested.field_list_uint64",
Expected: testContainer.Nested.FieldListUint64,
},
{
Path: ".nested.field_list_uint64[3]",
Expected: testContainer.Nested.FieldListUint64[3],
},
{
Path: ".nested.nested_list_field",
Expected: testContainer.Nested.NestedListField,
},
{
Path: ".nested.nested_list_field[0]",
Expected: testContainer.Nested.NestedListField[0],
},
{
Path: ".nested.nested_list_field[1]",
Expected: testContainer.Nested.NestedListField[1],
},
{
Path: ".nested.nested_list_field[2]",
Expected: testContainer.Nested.NestedListField[2],
},
// Variable list of variable-sized containers
{
Path: ".variable_container_list",
Expected: testContainer.VariableContainerList,
},
{
Path: ".variable_container_list[0]",
Expected: testContainer.VariableContainerList[0],
},
{
Path: ".variable_container_list[0].inner_1.field_list_uint64[1]",
Expected: testContainer.VariableContainerList[0].Inner_1.FieldListUint64[1],
},
{
Path: ".variable_container_list[0].inner_2.field_list_uint64[1]",
Expected: testContainer.VariableContainerList[0].Inner_2.FieldListUint64[1],
},
{
Path: ".variable_container_list[1]",
Expected: testContainer.VariableContainerList[1],
},
{
Path: ".variable_container_list[1].inner_1.field_list_uint64[1]",
Expected: testContainer.VariableContainerList[1].Inner_1.FieldListUint64[1],
},
{
Path: ".variable_container_list[1].inner_2.field_list_uint64[1]",
Expected: testContainer.VariableContainerList[1].Inner_2.FieldListUint64[1],
},
// Bitlist field
{
Path: ".bitlist_field",
Expected: testContainer.BitlistField,
},
// 2D bytes field
{
Path: ".nested_list_field",
Expected: testContainer.NestedListField,
},
{
Path: ".nested_list_field[0]",
Expected: testContainer.NestedListField[0],
},
{
Path: ".nested_list_field[1]",
Expected: testContainer.NestedListField[1],
},
{
Path: ".nested_list_field[2]",
Expected: testContainer.NestedListField[2],
},
// Fixed trailing field
{
Path: ".trailing_field",
Expected: testContainer.TrailingField,
},
},
}
}