Skip to content

Commit 4233704

Browse files
committed
fix: allow negative observations, error if bounds exceeded
1 parent 92e1eb8 commit 4233704

File tree

4 files changed

+329
-5
lines changed

4 files changed

+329
-5
lines changed

pkg/solana/marshal_signed_int.go

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// code from: https://github.com/smartcontractkit/chainlink-terra/blob/develop/pkg/terra/marshal_signed_int.go
2+
// will eventually be removed and replaced with a generalized version from libocr
3+
4+
package solana
5+
6+
import (
7+
"bytes"
8+
"fmt"
9+
"math/big"
10+
)
11+
12+
var i = big.NewInt
13+
14+
func bounds(numBytes uint) (*big.Int, *big.Int) {
15+
max := i(0).Sub(i(0).Lsh(i(1), numBytes*8-1), i(1)) // 2**(numBytes*8-1)- 1
16+
min := i(0).Sub(i(0).Neg(max), i(1)) // -2**(numBytes*8-1)
17+
return min, max
18+
}
19+
20+
// ToBigInt interprets bytes s as a big-endian signed integer
21+
// of size numBytes.
22+
func ToBigInt(s []byte, numBytes uint) (*big.Int, error) {
23+
if uint(len(s)) != numBytes {
24+
return nil, fmt.Errorf("invalid int length: expected %d got %d", numBytes, len(s))
25+
}
26+
val := (&big.Int{}).SetBytes(s)
27+
numBits := numBytes * 8
28+
_, max := bounds(numBytes)
29+
negative := val.Cmp(max) > 0
30+
if negative {
31+
// Get the complement wrt to 2^numBits
32+
maxUint := big.NewInt(1)
33+
maxUint.Lsh(maxUint, numBits)
34+
val.Sub(maxUint, val)
35+
val.Neg(val)
36+
}
37+
return val, nil
38+
}
39+
40+
// ToBytes converts *big.Int o into bytes as a big-endian signed
41+
// integer of size numBytes
42+
func ToBytes(o *big.Int, numBytes uint) ([]byte, error) {
43+
min, max := bounds(numBytes)
44+
if o.Cmp(max) > 0 || o.Cmp(min) < 0 {
45+
return nil, fmt.Errorf("value won't fit in int%v: 0x%x", numBytes*8, o)
46+
}
47+
negative := o.Sign() < 0
48+
val := (&big.Int{})
49+
numBits := numBytes * 8
50+
if negative {
51+
// compute two's complement as 2**numBits - abs(o) = 2**numBits + o
52+
val.SetInt64(1)
53+
val.Lsh(val, numBits)
54+
val.Add(val, o)
55+
} else {
56+
val.Set(o)
57+
}
58+
b := val.Bytes() // big-endian representation of abs(val)
59+
if uint(len(b)) > numBytes {
60+
return nil, fmt.Errorf("b must fit in %v bytes", numBytes)
61+
}
62+
b = bytes.Join([][]byte{bytes.Repeat([]byte{0}, int(numBytes)-len(b)), b}, []byte{})
63+
if uint(len(b)) != numBytes {
64+
return nil, fmt.Errorf("wrong length; there must be an error in the padding of b: %v", b)
65+
}
66+
return b, nil
67+
}

pkg/solana/marshal_signed_int_test.go

+186
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
package solana
2+
3+
import (
4+
"encoding/hex"
5+
"math/big"
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
func TestMarshalSignedInt(t *testing.T) {
13+
var tt = []struct {
14+
bytesVal string
15+
size uint
16+
expected *big.Int
17+
expectErr bool
18+
}{
19+
{
20+
"ffffffffffffffff",
21+
8,
22+
big.NewInt(-1),
23+
false,
24+
},
25+
{
26+
"fffffffffffffffe",
27+
8,
28+
big.NewInt(-2),
29+
false,
30+
},
31+
{
32+
"0000000000000000",
33+
8,
34+
big.NewInt(0),
35+
false,
36+
},
37+
{
38+
"0000000000000001",
39+
8,
40+
big.NewInt(1),
41+
false,
42+
},
43+
{
44+
"0000000000000002",
45+
8,
46+
big.NewInt(2),
47+
false,
48+
},
49+
{
50+
"7fffffffffffffff",
51+
8,
52+
big.NewInt(9223372036854775807), // 2^63 - 1
53+
false,
54+
},
55+
{
56+
"00000000000000000000000000000000",
57+
16,
58+
big.NewInt(0),
59+
false,
60+
},
61+
{
62+
"00000000000000000000000000000001",
63+
16,
64+
big.NewInt(1),
65+
false,
66+
},
67+
{
68+
"00000000000000000000000000000002",
69+
16,
70+
big.NewInt(2),
71+
false,
72+
},
73+
{
74+
"7fffffffffffffffffffffffffffffff", // 2^127 - 1
75+
16,
76+
big.NewInt(0).Sub(big.NewInt(0).Lsh(big.NewInt(1), 127), big.NewInt(1)),
77+
false,
78+
},
79+
{
80+
"ffffffffffffffffffffffffffffffff",
81+
16,
82+
big.NewInt(-1),
83+
false,
84+
},
85+
{
86+
"fffffffffffffffffffffffffffffffe",
87+
16,
88+
big.NewInt(-2),
89+
false,
90+
},
91+
{
92+
"000000000000000000000000000000000000000000000000",
93+
24,
94+
big.NewInt(0),
95+
false,
96+
},
97+
{
98+
"000000000000000000000000000000000000000000000001",
99+
24,
100+
big.NewInt(1),
101+
false,
102+
},
103+
{
104+
"000000000000000000000000000000000000000000000002",
105+
24,
106+
big.NewInt(2),
107+
false,
108+
},
109+
{
110+
"ffffffffffffffffffffffffffffffffffffffffffffffff",
111+
24,
112+
big.NewInt(-1),
113+
false,
114+
},
115+
{
116+
"fffffffffffffffffffffffffffffffffffffffffffffffe",
117+
24,
118+
big.NewInt(-2),
119+
false,
120+
},
121+
}
122+
for _, tc := range tt {
123+
tc := tc
124+
b, err := hex.DecodeString(tc.bytesVal)
125+
require.NoError(t, err)
126+
i, err := ToBigInt(b, tc.size)
127+
require.NoError(t, err)
128+
assert.Equal(t, i.String(), tc.expected.String())
129+
130+
// Marshalling back should give us the same bytes
131+
bAfter, err := ToBytes(i, tc.size)
132+
require.NoError(t, err)
133+
assert.Equal(t, tc.bytesVal, hex.EncodeToString(bAfter))
134+
}
135+
136+
var tt2 = []struct {
137+
o *big.Int
138+
numBytes uint
139+
expectErr bool
140+
}{
141+
{
142+
big.NewInt(128),
143+
1,
144+
true,
145+
},
146+
{
147+
big.NewInt(-129),
148+
1,
149+
true,
150+
},
151+
{
152+
big.NewInt(-128),
153+
1,
154+
false,
155+
},
156+
{
157+
big.NewInt(2147483648),
158+
4,
159+
true,
160+
},
161+
{
162+
big.NewInt(2147483647),
163+
4,
164+
false,
165+
},
166+
{
167+
big.NewInt(-2147483649),
168+
4,
169+
true,
170+
},
171+
{
172+
big.NewInt(-2147483648),
173+
4,
174+
false,
175+
},
176+
}
177+
for _, tc := range tt2 {
178+
tc := tc
179+
_, err := ToBytes(tc.o, tc.numBytes)
180+
if tc.expectErr {
181+
require.Error(t, err)
182+
} else {
183+
require.NoError(t, err)
184+
}
185+
}
186+
}

pkg/solana/report.go

+14-5
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"math/big"
88
"sort"
99

10+
"github.com/pkg/errors"
1011
"github.com/smartcontractkit/libocr/offchainreporting2/chains/evmutil"
1112
"github.com/smartcontractkit/libocr/offchainreporting2/reportingplugin/median"
1213
"github.com/smartcontractkit/libocr/offchainreporting2/types"
@@ -62,11 +63,19 @@ func (c ReportCodec) BuildReport(oo []median.ParsedAttributedObservation) (types
6263

6364
report = append(report, observers[:]...)
6465

65-
mBytes := make([]byte, MedianLen)
66-
report = append(report, median.FillBytes(mBytes)[:]...)
66+
// TODO: replace with generalized function from libocr
67+
medianBytes, err := ToBytes(median, uint(MedianLen))
68+
if err != nil {
69+
return nil, errors.Wrap(err, "error in ToBytes(median)")
70+
}
71+
report = append(report, medianBytes[:]...)
6772

68-
jBytes := make([]byte, JuelsLen)
69-
report = append(report, juelsPerFeeCoin.FillBytes(jBytes)[:]...)
73+
// TODO: replace with generalized function from libocr
74+
juelsPerFeeCoinBytes, err := ToBytes(juelsPerFeeCoin, uint(JuelsLen))
75+
if err != nil {
76+
return nil, errors.Wrap(err, "error in ToBytes(juelsPerFeeCoin)")
77+
}
78+
report = append(report, juelsPerFeeCoinBytes[:]...)
7079

7180
return types.Report(report), nil
7281
}
@@ -81,7 +90,7 @@ func (c ReportCodec) MedianFromReport(report types.Report) (*big.Int, error) {
8190
start := 4 + 1 + 32
8291
end := start + int(MedianLen)
8392
median := report[start:end]
84-
return big.NewInt(0).SetBytes(median), nil
93+
return ToBigInt(median, uint(MedianLen))
8594
}
8695

8796
// Create report digest using SHA256 hash fn

pkg/solana/report_test.go

+62
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ package solana
22

33
import (
44
"encoding/binary"
5+
"math"
56
"math/big"
67
"testing"
78
"time"
89

10+
bin "github.com/gagliardetto/binary"
911
"github.com/smartcontractkit/libocr/commontypes"
1012
"github.com/smartcontractkit/libocr/offchainreporting2/reportingplugin/median"
1113
"github.com/smartcontractkit/libocr/offchainreporting2/types"
@@ -108,3 +110,63 @@ func TestHashReport(t *testing.T) {
108110
assert.NoError(t, err)
109111
assert.Equal(t, mockHash, h)
110112
}
113+
114+
func TestNegativeMedianValue(t *testing.T) {
115+
c := ReportCodec{}
116+
oo := []median.ParsedAttributedObservation{
117+
median.ParsedAttributedObservation{
118+
Timestamp: uint32(time.Now().Unix()),
119+
Value: big.NewInt(-2),
120+
JuelsPerFeeCoin: big.NewInt(1),
121+
Observer: commontypes.OracleID(0),
122+
},
123+
}
124+
125+
// create report
126+
report, err := c.BuildReport(oo)
127+
assert.NoError(t, err)
128+
129+
// check report properly encoded negative number
130+
index := 4 + 1 + 32
131+
var medianFromRaw bin.Int128
132+
medianBytes := make([]byte, MedianLen)
133+
copy(medianBytes, report[index:index+int(MedianLen)])
134+
// flip order: bin decoder parses from little endian
135+
for i, j := 0, len(medianBytes)-1; i < j; i, j = i+1, j-1 {
136+
medianBytes[i], medianBytes[j] = medianBytes[j], medianBytes[i]
137+
}
138+
bin.NewBinDecoder(medianBytes).Decode(&medianFromRaw)
139+
assert.True(t, oo[0].Value.Cmp(medianFromRaw.BigInt()) == 0, "median observation in raw report does not match")
140+
141+
// check report can be parsed properly with a negative number
142+
res, err := c.MedianFromReport(report)
143+
assert.NoError(t, err)
144+
assert.True(t, oo[0].Value.Cmp(res) == 0)
145+
}
146+
147+
func TestReportHandleOverflow(t *testing.T) {
148+
// too large observation should not cause panic
149+
c := ReportCodec{}
150+
oo := []median.ParsedAttributedObservation{
151+
median.ParsedAttributedObservation{
152+
Timestamp: uint32(time.Now().Unix()),
153+
Value: big.NewInt(0).Lsh(big.NewInt(1), 127), // 1<<127
154+
JuelsPerFeeCoin: big.NewInt(0),
155+
Observer: commontypes.OracleID(0),
156+
},
157+
}
158+
_, err := c.BuildReport(oo)
159+
assert.Error(t, err)
160+
161+
// too large juelsPerFeeCoin should not cause panic
162+
oo = []median.ParsedAttributedObservation{
163+
median.ParsedAttributedObservation{
164+
Timestamp: uint32(time.Now().Unix()),
165+
Value: big.NewInt(0),
166+
JuelsPerFeeCoin: big.NewInt(0).Add(big.NewInt(math.MaxInt64), big.NewInt(1)),
167+
Observer: commontypes.OracleID(0),
168+
},
169+
}
170+
_, err = c.BuildReport(oo)
171+
assert.Error(t, err)
172+
}

0 commit comments

Comments
 (0)