Skip to content

Commit 5a8e0e1

Browse files
authored
fix: Chunk Lens blocks if larger than 3MB (#145)
1 parent 6ffef28 commit 5a8e0e1

10 files changed

Lines changed: 220 additions & 25 deletions

File tree

host-go/node/node.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,10 @@ func New(ctx context.Context, opts ...Option) (*Node, error) {
6969
o.IndexstoreNamespace = immutable.Some("i")
7070
}
7171

72+
if !o.MaxBlockSize.HasValue() {
73+
o.MaxBlockSize = immutable.Some(1024 * 1024 * 3)
74+
}
75+
7276
repo := repository.NewRepository(o.PoolSize.Value(), o.Runtime.Value(), &repositoryTxnSource{src: o.TxnSource.Value()})
7377

7478
node, err := createNode(
@@ -78,6 +82,7 @@ func New(ctx context.Context, opts ...Option) (*Node, error) {
7882
repo,
7983
o.BlockstoreNamespace.Value(),
8084
o.BlockstoreChunkSize,
85+
o.MaxBlockSize,
8186
o.IndexstoreNamespace.Value(),
8287
),
8388
repo,

host-go/node/node_p2p.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ type Options struct {
2727
Runtime immutable.Option[module.Runtime]
2828
BlockstoreNamespace immutable.Option[string]
2929
BlockstoreChunkSize immutable.Option[int]
30+
MaxBlockSize immutable.Option[int]
3031
IndexstoreNamespace immutable.Option[string]
3132
P2P immutable.Option[p2p.Host]
3233
DisableP2P bool

host-go/node/option.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,3 +70,15 @@ func WithBlockstoreChunkSize(blockstoreChunkSize int) Option {
7070
opts.BlockstoreChunkSize = immutable.Some(blockstoreChunkSize)
7171
}
7272
}
73+
74+
// WithMaxBlockSize will limit the size of lens blocks to the given length if provided.
75+
//
76+
// IPFS implmentations limit the size of the blocks that they are willing to transport
77+
// and can silently fail if they come across a block that is too large (~4MB).
78+
//
79+
// This option defaults to 3MB.
80+
func WithMaxBlockSize(maxBlockSize int) Option {
81+
return func(opts *Options) {
82+
opts.MaxBlockSize = immutable.Some(maxBlockSize)
83+
}
84+
}

host-go/store/block.go

Lines changed: 108 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
package store
66

77
import (
8+
"bytes"
89
"context"
910
"encoding/json"
11+
"slices"
1012
"sort"
1113

1214
cid "github.com/ipfs/go-cid"
@@ -46,21 +48,26 @@ func init() {
4648
LensBlockSchema, LensBlockSchemaPrototype = mustSetSchema(
4749
"lensBlock",
4850
&LensBlock{},
51+
&Chunks{},
4952
)
5053
}
5154

52-
func mustSetSchema(schemaName string, schema schemaDefinition) (schema.Type, ipld.NodePrototype) {
53-
ts, err := ipld.LoadSchemaBytes(schema.IPLDSchemaBytes())
55+
func mustSetSchema(schemaName string, schemas ...schemaDefinition) (schema.Type, ipld.NodePrototype) {
56+
schemaBytes := make([][]byte, 0, len(schemas))
57+
for _, s := range schemas {
58+
schemaBytes = append(schemaBytes, s.IPLDSchemaBytes())
59+
}
60+
61+
ts, err := ipld.LoadSchemaBytes(bytes.Join(schemaBytes, nil))
5462
if err != nil {
5563
panic(err)
5664
}
57-
5865
schemaType := ts.TypeByName(schemaName)
5966

6067
// Calling bindnode.Prototype here ensure that [Block] and all the types it contains
6168
// are compatible with the IPLD schema defined by [schemaDefinition].
6269
// If [Block] and [schemaType] do not match, this will panic.
63-
proto := bindnode.Prototype(schema, schemaType)
70+
proto := bindnode.Prototype(schemas[0], schemaType)
6471

6572
return schemaType, proto.Representation()
6673
}
@@ -107,8 +114,84 @@ var _ schemaDefinition = (*ModuleBlock)(nil)
107114
//
108115
// A single LensBlock maybe referenced by an unlimited number of lens modules/configurations.
109116
type LensBlock struct {
117+
// If the total number of bytes is deemed too large for ipfs to transport we chunk the block
118+
// into multiple child blocks.
119+
//
120+
// This is governed by our `maxBlockSize` parameter/option.
121+
Chunks *Chunks
122+
110123
// WasmBytes is the executable wasm binary of the lens.
111-
WasmBytes []byte
124+
//
125+
// Or, if the bytes are too large for ipfs to transport, a chunk of those bytes.
126+
WasmBytes *[]byte
127+
}
128+
129+
var _ schemaDefinition = (*LensBlock)(nil)
130+
131+
type Chunks []datamodel.Link
132+
133+
var _ schemaDefinition = (*Chunks)(nil)
134+
135+
func storeLensBlock(
136+
ctx context.Context,
137+
linkSys *linking.LinkSystem,
138+
wasmBytes []byte,
139+
maxBlockSize int,
140+
) (datamodel.Link, error) {
141+
chunkBytes := [][]byte{}
142+
for chunk := range slices.Chunk(wasmBytes, maxBlockSize) {
143+
chunkBytes = append(chunkBytes, chunk)
144+
}
145+
146+
if len(chunkBytes) == 1 {
147+
block := LensBlock{
148+
WasmBytes: &chunkBytes[0],
149+
}
150+
return linkSys.Store(linking.LinkContext{Ctx: ctx}, getLinkPrototype(), block.generateNode())
151+
}
152+
153+
links := []datamodel.Link{}
154+
for _, chunk := range chunkBytes {
155+
block := &LensBlock{
156+
WasmBytes: &chunk,
157+
}
158+
159+
link, err := linkSys.Store(linking.LinkContext{Ctx: ctx}, getLinkPrototype(), block.generateNode())
160+
if err != nil {
161+
return nil, err
162+
}
163+
164+
links = append(links, link)
165+
}
166+
167+
chunkLinks := Chunks(links)
168+
block := LensBlock{
169+
Chunks: &chunkLinks,
170+
}
171+
return linkSys.Store(linking.LinkContext{Ctx: ctx}, getLinkPrototype(), block.generateNode())
172+
}
173+
174+
func (b *LensBlock) Bytes(ctx context.Context, linkSys *linking.LinkSystem) ([]byte, error) {
175+
switch {
176+
case b.WasmBytes != nil:
177+
return *b.WasmBytes, nil
178+
case b.Chunks != nil:
179+
var buf bytes.Buffer
180+
for _, chunk := range *b.Chunks {
181+
lensNode, err := linkSys.Load(linking.LinkContext{Ctx: ctx}, chunk, LensBlockSchemaPrototype)
182+
if err != nil {
183+
return nil, err
184+
}
185+
lensBlock := bindnode.Unwrap(lensNode).(*LensBlock)
186+
b, err := lensBlock.Bytes(ctx, linkSys)
187+
if err != nil {
188+
return nil, err
189+
}
190+
buf.Write(b)
191+
}
192+
return buf.Bytes(), nil
193+
}
194+
return nil, nil
112195
}
113196

114197
var _ schemaDefinition = (*LensBlock)(nil)
@@ -137,9 +220,16 @@ func (b *ModuleBlock) IPLDSchemaBytes() []byte {
137220

138221
func (b *LensBlock) IPLDSchemaBytes() []byte {
139222
return []byte(`
140-
type lensBlock struct {
141-
wasmBytes Bytes
142-
}
223+
type lensBlock union {
224+
| chunks "chunks"
225+
| Bytes "wasmBytes"
226+
} representation keyed
227+
`)
228+
}
229+
230+
func (b *Chunks) IPLDSchemaBytes() []byte {
231+
return []byte(`
232+
type chunks [Link]
143233
`)
144234
}
145235

@@ -185,15 +275,20 @@ func LoadLensModel(ctx context.Context, linkSys *linking.LinkSystem, cid cid.Cid
185275
}
186276
}
187277

188-
lensNode, err := linkSys.Load(ipld.LinkContext{Ctx: ctx}, moduleBlock.Lens, LensBlockSchemaPrototype)
278+
lensNode, err := linkSys.Load(linking.LinkContext{Ctx: ctx}, moduleBlock.Lens, LensBlockSchemaPrototype)
189279
if err != nil {
190280
return model.Lens{}, err
191281
}
192282
lensBlock := bindnode.Unwrap(lensNode).(*LensBlock)
193283

284+
wasmBytes, err := lensBlock.Bytes(ctx, linkSys)
285+
if err != nil {
286+
return model.Lens{}, err
287+
}
288+
194289
var path string
195-
if len(lensBlock.WasmBytes) != 0 {
196-
path = "data:application/octet-stream," + string(lensBlock.WasmBytes)
290+
if len(wasmBytes) != 0 {
291+
path = "data:application/octet-stream," + string(wasmBytes)
197292
}
198293

199294
result.Lenses = append(result.Lenses, model.LensModule{
@@ -209,6 +304,7 @@ func LoadLensModel(ctx context.Context, linkSys *linking.LinkSystem, cid cid.Cid
209304
func writeConfigBlock(
210305
ctx context.Context,
211306
linkSys *linking.LinkSystem,
307+
maxBlockSize int,
212308
cfg model.Lens,
213309
) (datamodel.Link, error) {
214310
moduleLinks := make([]datamodel.Link, 0, len(cfg.Lenses))
@@ -218,11 +314,8 @@ func writeConfigBlock(
218314
if err != nil {
219315
return nil, err
220316
}
221-
lensBlock := LensBlock{
222-
WasmBytes: wasmBytes,
223-
}
224317

225-
lensLink, err := linkSys.Store(linking.LinkContext{Ctx: ctx}, getLinkPrototype(), lensBlock.generateNode())
318+
lensLink, err := storeLensBlock(ctx, linkSys, wasmBytes, maxBlockSize)
226319
if err != nil {
227320
return nil, err
228321
}

host-go/store/store.go

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ import (
2424
"github.com/sourcenetwork/lens/host-go/repository"
2525
)
2626

27+
const defaultBlockMaxSize int = 1024 * 1024 * 3
28+
2729
type Store interface {
2830
// Add stores the given Lens and returns its content ID.
2931
//
@@ -78,13 +80,22 @@ func New(
7880
runtime module.Runtime,
7981
blockstoreNamespace string,
8082
blockstoreChunksize immutable.Option[int],
83+
maxBlockSize immutable.Option[int],
8184
indexstoreNamespace string,
8285
) TxnStore {
86+
var maxSize int
87+
if maxBlockSize.HasValue() {
88+
maxSize = maxBlockSize.Value()
89+
} else {
90+
maxSize = defaultBlockMaxSize
91+
}
92+
8393
return &implicitTxnStore{
8494
txnSource: txnSource,
8595
repository: repository.NewRepository(poolSize, runtime, &repositoryTxnSource{src: txnSource}),
8696
blockstoreNamespace: blockstoreNamespace,
8797
blockstoreChunksize: blockstoreChunksize,
98+
maxBlockSize: maxSize,
8899
indexstoreNamespace: indexstoreNamespace,
89100
}
90101
}
@@ -97,19 +108,28 @@ func NewWithRepository(
97108
repository repository.TxnRepository,
98109
blockstoreNamespace string,
99110
blockstoreChunksize immutable.Option[int],
111+
maxBlockSize immutable.Option[int],
100112
indexstoreNamespace string,
101113
) TxnStore {
114+
var maxSize int
115+
if maxBlockSize.HasValue() {
116+
maxSize = maxBlockSize.Value()
117+
} else {
118+
maxSize = defaultBlockMaxSize
119+
}
120+
102121
return &implicitTxnStore{
103122
txnSource: txnSource,
104123
repository: repository,
105124
blockstoreNamespace: blockstoreNamespace,
106125
blockstoreChunksize: blockstoreChunksize,
126+
maxBlockSize: maxSize,
107127
indexstoreNamespace: indexstoreNamespace,
108128
}
109129
}
110130

111131
func add(ctx context.Context, cfg model.Lens, txn *txn) (cid.Cid, error) {
112-
configLink, err := writeConfigBlock(ctx, txn.linkSystem, cfg)
132+
configLink, err := writeConfigBlock(ctx, txn.linkSystem, txn.maxBlockSize, cfg)
113133
if err != nil {
114134
return cid.Undef, err
115135
}

host-go/store/txn.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,10 @@ type Txn interface {
2323

2424
type txn struct {
2525
repository.Txn
26-
linkSystem *linking.LinkSystem
27-
indexstore corekv.ReaderWriter
28-
repository repository.Repository
26+
linkSystem *linking.LinkSystem
27+
maxBlockSize int
28+
indexstore corekv.ReaderWriter
29+
repository repository.Repository
2930
}
3031

3132
// repositoryTxnSource wraps a `TxnSource` so that it satisfies the `repository.TxnSource`

host-go/store/txn_store.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ type implicitTxnStore struct {
2424
blockstoreNamespace string
2525
blockstoreChunksize immutable.Option[int]
2626
indexstoreNamespace string
27+
maxBlockSize int
2728
}
2829

2930
type explicitTxnStore struct {
@@ -63,10 +64,11 @@ func (s *implicitTxnStore) wrapTxn(t Txn) *txn {
6364
}
6465

6566
return &txn{
66-
Txn: t,
67-
linkSystem: makeLinkSystem(bStore),
68-
indexstore: namespace.Wrap(t, []byte(s.indexstoreNamespace)),
69-
repository: s.repository.WithTxn(t),
67+
Txn: t,
68+
maxBlockSize: s.maxBlockSize,
69+
linkSystem: makeLinkSystem(bStore),
70+
indexstore: namespace.Wrap(t, []byte(s.indexstoreNamespace)),
71+
repository: s.repository.WithTxn(t),
7072
}
7173
}
7274

0 commit comments

Comments
 (0)