Skip to content

Commit 517b03b

Browse files
authored
fix: bigint support (#27)
This PR fixes support for big ints. Instead of leaning on a custom big int type (`go-state-types/big`), we use native `math/big`. It also adds support for big ints when marshaling to/from JSON. JSON encoded numbers do not have a fixed size, so it's impossible to distingish between `int64` and `big.Int` when decoding, without inventing a custom representation of these different types. The decision here is to decode a number to `int64` when the value fits, but otherwise use a `big.Int`. The trade off is that when encoding "small" big ints, you'll get back an `int64` when you decode i.e. they won't round trip. If you need the round trip to work you should use `dag-json-gen` to generate encoders/decoders. Alternatively use your own custom type, or `string` or `[]bytes` to represent the value. Related alanshaw/dag-json-gen#1 for negative bigint support.
1 parent eda937b commit 517b03b

4 files changed

Lines changed: 57 additions & 61 deletions

File tree

go.mod

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,8 @@ module github.com/fil-forge/ucantone
22

33
go 1.25.0
44

5-
// replace "github.com/alanshaw/dag-json-gen" => ../dag-json-gen
6-
75
require (
86
github.com/alanshaw/dag-json-gen v0.0.4
9-
github.com/filecoin-project/go-state-types v0.18.0
107
github.com/gobwas/glob v0.2.3
118
github.com/ipfs/go-cid v0.6.0
129
github.com/multiformats/go-multibase v0.2.0
@@ -32,6 +29,7 @@ require (
3229
gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect
3330
golang.org/x/crypto v0.49.0 // indirect
3431
golang.org/x/sys v0.42.0 // indirect
32+
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
3533
gopkg.in/yaml.v3 v3.0.1 // indirect
3634
lukechampine.com/blake3 v1.4.1 // indirect
3735
pitr.ca/jsontokenizer v0.3.0 // indirect

go.sum

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,17 @@ github.com/alanshaw/dag-json-gen v0.0.4/go.mod h1:rXxWw0SItP9QjxpRMpkju66h0KumF7
33
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
44
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
55
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
6-
github.com/filecoin-project/go-state-types v0.18.0 h1:oDcjihXRlf2cM176atZzllp79Zc+kcbiuQM9DPL/1a4=
7-
github.com/filecoin-project/go-state-types v0.18.0/go.mod h1:CcyG4ZQRDWW+QUY2WDf1KtVDRN7W4twjsfgnGbQfJVI=
86
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
97
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
108
github.com/ipfs/go-cid v0.6.0 h1:DlOReBV1xhHBhhfy/gBNNTSyfOM6rLiIx9J7A4DGf30=
119
github.com/ipfs/go-cid v0.6.0/go.mod h1:NC4kS1LZjzfhK40UGmpXv5/qD2kcMzACYJNntCUiDhQ=
1210
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
1311
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
12+
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
1413
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
1514
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
15+
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
16+
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
1617
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
1718
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
1819
github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=

ipld/datamodel/any.go

Lines changed: 31 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@ import (
77
"fmt"
88
"io"
99
"maps"
10+
"math/big"
1011
"reflect"
1112
"slices"
1213

1314
jsg "github.com/alanshaw/dag-json-gen"
1415
"github.com/fil-forge/ucantone/ipld"
15-
"github.com/filecoin-project/go-state-types/big"
1616
"github.com/ipfs/go-cid"
1717
cbg "github.com/whyrusleeping/cbor-gen"
1818
)
@@ -23,9 +23,8 @@ import (
2323
// - Null (nil)
2424
// - Boolean (bool)
2525
// - Integer (int64, int)
26-
// - BigInteger (go-state-types/big.Int) — encoded as a CBOR bignum: tag 2
26+
// - BigInteger (math/big) — encoded as a CBOR bignum: tag 2
2727
// for non-negative values and tag 3 for negative values (RFC 8949).
28-
// CBOR only; big integers are not supported in DAG-JSON.
2928
// - String (string)
3029
// - Bytes ([]byte)
3130
// - List ([]Any)
@@ -44,7 +43,7 @@ type Any struct {
4443
// - bool
4544
// - int
4645
// - int64
47-
// - big.Int (go-state-types/big; CBOR only)
46+
// - math/big.Int
4847
// - string
4948
// - []byte
5049
// - slice
@@ -72,13 +71,9 @@ func (a *Any) MarshalCBOR(w io.Writer) error {
7271
case int:
7372
return cbg.CborInt(v).MarshalCBOR(w)
7473
case big.Int:
75-
return marshalCborBigInt(w, v)
74+
return marshalCborBigInt(w, &v)
7675
case *big.Int:
77-
if v == nil {
78-
_, err := w.Write(cbg.CborNull)
79-
return err
80-
}
81-
return marshalCborBigInt(w, *v)
76+
return marshalCborBigInt(w, v)
8277
case bool:
8378
return cbg.CborBool(v).MarshalCBOR(w)
8479
case cid.Cid:
@@ -171,14 +166,14 @@ func (a *Any) UnmarshalCBOR(r io.Reader) (err error) {
171166
if err != nil {
172167
return err
173168
}
174-
a.Value = big.PositiveFromUnsignedBytes(b)
169+
a.Value = big.NewInt(0).SetBytes(b)
175170
return nil
176171
case 3: // CBOR negative bignum (tag 3 + byte string): value = -1 - n
177172
b, err := readBignumBytes(pr)
178173
if err != nil {
179174
return err
180175
}
181-
a.Value = big.Sub(big.NewInt(-1), big.PositiveFromUnsignedBytes(b))
176+
a.Value = big.NewInt(0).Sub(big.NewInt(-1), big.NewInt(0).SetBytes(b))
182177
return nil
183178
case 42:
184179
cbc := cbg.CborCid{}
@@ -273,8 +268,13 @@ func (a *Any) MarshalDagJSON(w io.Writer) error {
273268
return jw.WriteInt64(v)
274269
case int:
275270
return jw.WriteInt64(int64(v))
276-
case big.Int, *big.Int:
277-
return fmt.Errorf("big integers are not supported in DAG-JSON")
271+
case big.Int:
272+
return jw.WriteBigInt(&v)
273+
case *big.Int:
274+
if v == nil {
275+
return jw.WriteNull()
276+
}
277+
return jw.WriteBigInt(v)
278278
case bool:
279279
return jw.WriteBool(v)
280280
case cid.Cid:
@@ -344,11 +344,21 @@ func (a *Any) UnmarshalDagJSON(r io.Reader) (err error) {
344344
}
345345
a.Value = v
346346
case "number":
347-
v, err := jr.ReadNumberAsInt64()
347+
v, err := jr.ReadNumberAsBigInt(jsg.MaxLength)
348348
if err != nil {
349349
return err
350350
}
351-
a.Value = v
351+
// There's no distinction in JSON between int64 and big.Int, there is just
352+
// number. If the value fits in an int64, return it as an int64. It means
353+
// we can't round trip a big.Int that fits in an int64, but there is not
354+
// really a good alternative here without inventing our own encoding for
355+
// big.Int in JSON. If you need to round trip, use dag-json-gen to generate
356+
// your encoders/decoders or use string or bytes or your own type instead.
357+
if v.IsInt64() {
358+
a.Value = v.Int64()
359+
} else {
360+
a.Value = v
361+
}
352362
case "array":
353363
if err := jr.ReadArrayOpen(); err != nil {
354364
return err
@@ -448,26 +458,26 @@ func (a *Any) UnmarshalDagJSON(r io.Reader) (err error) {
448458
return nil
449459
}
450460

451-
// marshalCborBigInt writes a big.Int as a CBOR bignum (RFC 8949): tag 2 for
461+
// marshalCborBigInt writes a [big.Int] as a CBOR bignum (RFC 8949): tag 2 for
452462
// non-negative values and tag 3 for negative values, each followed by a byte
453463
// string holding the big-endian magnitude. A negative value v is encoded as
454464
// the magnitude of n = -1 - v, so it round-trips via value = -1 - n on decode.
455-
func marshalCborBigInt(w io.Writer, v big.Int) error {
456-
if v.Nil() {
465+
func marshalCborBigInt(w io.Writer, v *big.Int) error {
466+
if v == nil {
457467
_, err := w.Write(cbg.CborNull)
458468
return err
459469
}
460470
tag := uint64(2)
461471
mag := v // non-negative: encode the magnitude directly
462472
if v.Sign() < 0 {
463473
tag = 3
464-
mag = big.Sub(big.NewInt(-1), v) // n = -1 - v
474+
mag = big.NewInt(0).Sub(big.NewInt(-1), v) // n = -1 - v
465475
}
466476
cw := cbg.NewCborWriter(w)
467477
if err := cw.WriteMajorTypeHeader(cbg.MajTag, tag); err != nil {
468478
return err
469479
}
470-
b := mag.Int.Bytes() // raw big-endian magnitude (no sign prefix)
480+
b := mag.Bytes() // raw big-endian magnitude (no sign prefix)
471481
if err := cw.WriteMajorTypeHeader(cbg.MajByteString, uint64(len(b))); err != nil {
472482
return err
473483
}

ipld/datamodel/any_test.go

Lines changed: 22 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,23 @@ package datamodel_test
33
import (
44
"bytes"
55
"fmt"
6+
"math/big"
7+
"slices"
68
"testing"
79

810
"github.com/fil-forge/ucantone/ipld"
911
"github.com/fil-forge/ucantone/ipld/datamodel"
1012
"github.com/fil-forge/ucantone/testutil"
11-
"github.com/filecoin-project/go-state-types/big"
1213
"github.com/stretchr/testify/require"
1314
)
1415

1516
func TestAny(t *testing.T) {
17+
gtMaxUint64, ok := big.NewInt(0).SetString("340282366920938463463374607431768211457", 10) // > maxUint64, positive
18+
require.True(t, ok)
19+
20+
ltMinInt64, ok := big.NewInt(0).SetString("-340282366920938463463374607431768211457", 10) // < minInt64, negative
21+
require.True(t, ok)
22+
1623
values := []any{
1724
int64(138),
1825
true,
@@ -27,12 +34,23 @@ func TestAny(t *testing.T) {
2734
"str": "X",
2835
"bytes": []byte{2},
2936
},
37+
gtMaxUint64,
38+
ltMinInt64,
3039
// map[string]cid.Cid{
3140
// "await/ok": testutil.RandomCID(t),
3241
// },
3342
}
3443

35-
for _, v := range values {
44+
// These values will only round trip as CBOR since JSON encoding does not
45+
// allow us to distinguish fixed size integers and arbitrary size bignums.
46+
cborValues := append(
47+
slices.Clone(values),
48+
big.NewInt(0),
49+
big.NewInt(138),
50+
big.NewInt(-138),
51+
)
52+
53+
for _, v := range cborValues {
3654
t.Run(fmt.Sprintf("dag-cbor %T", v), func(t *testing.T) {
3755
initial := datamodel.NewAny(v)
3856

@@ -45,7 +63,9 @@ func TestAny(t *testing.T) {
4563
require.NoError(t, err)
4664
require.Equal(t, v, decodedCBOR.Value)
4765
})
66+
}
4867

68+
for _, v := range values {
4969
t.Run(fmt.Sprintf("dag-json %T", v), func(t *testing.T) {
5070
initial := datamodel.NewAny(v)
5171

@@ -62,36 +82,3 @@ func TestAny(t *testing.T) {
6282
})
6383
}
6484
}
65-
66-
// big.Int is CBOR only (not supported in DAG-JSON), so it round-trips through
67-
// its own test rather than the shared list above.
68-
func TestAnyBigInt(t *testing.T) {
69-
values := []big.Int{
70-
big.NewInt(0),
71-
big.NewInt(138),
72-
big.NewInt(-138),
73-
big.MustFromString("340282366920938463463374607431768211457"), // > maxUint64, positive
74-
big.MustFromString("-340282366920938463463374607431768211457"), // < minInt64, negative
75-
}
76-
77-
for _, v := range values {
78-
t.Run(fmt.Sprintf("dag-cbor %s", v), func(t *testing.T) {
79-
initial := datamodel.NewAny(v)
80-
81-
var buf bytes.Buffer
82-
err := initial.MarshalCBOR(&buf)
83-
require.NoError(t, err)
84-
85-
var decoded datamodel.Any
86-
err = decoded.UnmarshalCBOR(&buf)
87-
require.NoError(t, err)
88-
require.Equal(t, v, decoded.Value)
89-
})
90-
91-
t.Run(fmt.Sprintf("dag-json unsupported %s", v), func(t *testing.T) {
92-
var buf bytes.Buffer
93-
err := datamodel.NewAny(v).MarshalDagJSON(&buf)
94-
require.Error(t, err)
95-
})
96-
}
97-
}

0 commit comments

Comments
 (0)