Skip to content

Commit 354581a

Browse files
committed
Malleable Sapient lib
1 parent ecab97c commit 354581a

File tree

13 files changed

+1989
-0
lines changed

13 files changed

+1989
-0
lines changed

lib/sapient/malleable/README.md

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# Malleable Sapient (Go)
2+
3+
Build MalleableSapient signatures by locating byte ranges in call data, and optionally compute the image hash.
4+
5+
## Usage
6+
7+
```go
8+
payload := v3.NewCallsPayload(...)
9+
10+
permitValue := malleable.NewPath().
11+
CallData(0).
12+
ABI(trailsABI, "hydrateExecute").
13+
ArgBytesData("packedPayload").
14+
EncodedCallsPayload().
15+
EncodedCallData(0).
16+
ABI(erc2612ABI, "permit").
17+
ArgSlot("value").
18+
AsSelector()
19+
20+
transferValue := malleable.NewPath().
21+
CallData(0).
22+
ABI(trailsABI, "hydrateExecute").
23+
ArgBytesData("packedPayload").
24+
EncodedCallsPayload().
25+
EncodedCallData(1).
26+
ABI(erc20ABI, "transferFrom").
27+
ArgSlot("_value").
28+
AsSelector()
29+
30+
b := malleable.NewBuilder(payload, &malleable.BuilderOptions{
31+
ValidateRepeats: true,
32+
MergeAdjacentStatic: true,
33+
})
34+
35+
b.Repeat(permitValue, transferValue) // repeat constraint
36+
37+
// mark other malleable fields
38+
b.Malleable(malleable.NewPath().
39+
CallData(0).
40+
ABI(trailsABI, "hydrateExecute").
41+
ArgBytesData("packedPayload").
42+
EncodedCallsPayload().
43+
EncodedCallData(0).
44+
ABI(erc2612ABI, "permit").
45+
ArgSlot("deadline").
46+
AsSelector(),
47+
)
48+
49+
sig, _, err := b.Build()
50+
```
51+
52+
If ABI params are unnamed, use index-based selectors:
53+
54+
```go
55+
value := malleable.NewPath().
56+
CallData(0).
57+
ABI(erc20ABI, "transferFrom").
58+
ArgSlotIndex(2).
59+
AsSelector()
60+
```
61+
62+
Compute the image hash:
63+
64+
```go
65+
hash, err := malleable.ComputeImageHash(payload, sig, chainID)
66+
```

lib/sapient/malleable/builder.go

Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
package malleable
2+
3+
import (
4+
"fmt"
5+
"sort"
6+
7+
"github.com/0xsequence/ethkit/go-ethereum/crypto"
8+
v3 "github.com/0xsequence/go-sequence/core/v3"
9+
)
10+
11+
type BuilderOptions struct {
12+
ValidateRepeats bool
13+
MergeAdjacentStatic bool
14+
MaxOffset uint32
15+
MaxSize uint32
16+
}
17+
18+
type Builder struct {
19+
payload *v3.CallsPayload
20+
options BuilderOptions
21+
malleable []Selector
22+
repeats []repeatSelector
23+
}
24+
25+
type repeatSelector struct {
26+
a Selector
27+
b Selector
28+
}
29+
30+
type Plan struct {
31+
Static []StaticSection
32+
Repeat []RepeatSection
33+
}
34+
35+
func (p *Plan) DebugString() string {
36+
out := "static:"
37+
for _, s := range p.Static {
38+
out += fmt.Sprintf(" [t=%d c=%d s=%d]", s.TIndex, s.CIndex, s.Size)
39+
}
40+
out += " repeat:"
41+
for _, r := range p.Repeat {
42+
out += fmt.Sprintf(" [t=%d c=%d s=%d t2=%d c2=%d]", r.TIndex, r.CIndex, r.Size, r.TIndex2, r.CIndex2)
43+
}
44+
return out
45+
}
46+
47+
func NewBuilder(payload *v3.CallsPayload, opts *BuilderOptions) *Builder {
48+
options := BuilderOptions{
49+
MaxOffset: 0xFFFF,
50+
MaxSize: 0xFFFF,
51+
ValidateRepeats: false,
52+
MergeAdjacentStatic: true,
53+
}
54+
if opts != nil {
55+
if opts.MaxOffset != 0 {
56+
options.MaxOffset = opts.MaxOffset
57+
}
58+
if opts.MaxSize != 0 {
59+
options.MaxSize = opts.MaxSize
60+
}
61+
options.ValidateRepeats = opts.ValidateRepeats
62+
options.MergeAdjacentStatic = opts.MergeAdjacentStatic
63+
}
64+
65+
return &Builder{
66+
payload: payload,
67+
options: options,
68+
}
69+
}
70+
71+
func (b *Builder) Malleable(sel Selector) *Builder {
72+
b.malleable = append(b.malleable, sel)
73+
return b
74+
}
75+
76+
func (b *Builder) Repeat(a Selector, b2 Selector) *Builder {
77+
b.repeats = append(b.repeats, repeatSelector{a: a, b: b2})
78+
return b
79+
}
80+
81+
func (b *Builder) Build() ([]byte, *Plan, error) {
82+
if b.payload == nil {
83+
return nil, nil, fmt.Errorf("payload is nil")
84+
}
85+
if len(b.payload.Calls) > 128 {
86+
return nil, nil, fmt.Errorf("too many calls (%d): tindex is 7-bit", len(b.payload.Calls))
87+
}
88+
89+
exByCall := make([][]Span, len(b.payload.Calls))
90+
91+
addExclude := func(r ByteRange) error {
92+
if r.CallIndex < 0 || r.CallIndex >= len(b.payload.Calls) {
93+
return fmt.Errorf("tindex out of range: %d", r.CallIndex)
94+
}
95+
dataLen := len(b.payload.Calls[r.CallIndex].Data)
96+
if r.Offset < 0 || r.Size < 0 || r.Offset+r.Size > dataLen {
97+
return fmt.Errorf("span out of bounds: [%d,%d) > %d", r.Offset, r.Offset+r.Size, dataLen)
98+
}
99+
exByCall[r.CallIndex] = append(exByCall[r.CallIndex], Span{Start: r.Offset, Len: r.Size})
100+
return nil
101+
}
102+
103+
for _, sel := range b.malleable {
104+
ranges, err := sel.Resolve(b.payload)
105+
if err != nil {
106+
return nil, nil, fmt.Errorf("malleable %s: %w", sel.String(), err)
107+
}
108+
for _, r := range ranges {
109+
if err := addExclude(r); err != nil {
110+
return nil, nil, fmt.Errorf("malleable %s: %w", sel.String(), err)
111+
}
112+
}
113+
}
114+
115+
var repeats []RepeatSection
116+
for _, rp := range b.repeats {
117+
rangesA, err := rp.a.Resolve(b.payload)
118+
if err != nil {
119+
return nil, nil, fmt.Errorf("repeat a %s: %w", rp.a.String(), err)
120+
}
121+
rangesB, err := rp.b.Resolve(b.payload)
122+
if err != nil {
123+
return nil, nil, fmt.Errorf("repeat b %s: %w", rp.b.String(), err)
124+
}
125+
if len(rangesA) != 1 || len(rangesB) != 1 {
126+
return nil, nil, fmt.Errorf("repeat expects single range per selector")
127+
}
128+
a := rangesA[0]
129+
bb := rangesB[0]
130+
if a.Size != bb.Size {
131+
return nil, nil, fmt.Errorf("repeat size mismatch: %d vs %d", a.Size, bb.Size)
132+
}
133+
if err := addExclude(a); err != nil {
134+
return nil, nil, fmt.Errorf("repeat a: %w", err)
135+
}
136+
if err := addExclude(bb); err != nil {
137+
return nil, nil, fmt.Errorf("repeat b: %w", err)
138+
}
139+
if b.options.ValidateRepeats {
140+
sectionA, err := a.Slice(b.payload)
141+
if err != nil {
142+
return nil, nil, err
143+
}
144+
sectionB, err := bb.Slice(b.payload)
145+
if err != nil {
146+
return nil, nil, err
147+
}
148+
if crypto.Keccak256Hash(sectionA) != crypto.Keccak256Hash(sectionB) {
149+
return nil, nil, fmt.Errorf("repeat section mismatch")
150+
}
151+
}
152+
repeats = append(repeats, RepeatSection{
153+
TIndex: uint8(a.CallIndex),
154+
CIndex: uint16(a.Offset),
155+
Size: uint16(a.Size),
156+
TIndex2: uint8(bb.CallIndex),
157+
CIndex2: uint16(bb.Offset),
158+
})
159+
}
160+
161+
var statics []SpanWithCall
162+
for t := range b.payload.Calls {
163+
length := len(b.payload.Calls[t].Data)
164+
ex := mergeSpans(exByCall[t])
165+
cursor := 0
166+
for _, s := range ex {
167+
if cursor < s.Start {
168+
statics = append(statics, SpanWithCall{CallIndex: t, Span: Span{Start: cursor, Len: s.Start - cursor}})
169+
}
170+
cursor = max(cursor, s.Start+s.Len)
171+
}
172+
if cursor < length {
173+
statics = append(statics, SpanWithCall{CallIndex: t, Span: Span{Start: cursor, Len: length - cursor}})
174+
}
175+
}
176+
177+
sort.Slice(statics, func(i, j int) bool {
178+
if statics[i].CallIndex != statics[j].CallIndex {
179+
return statics[i].CallIndex < statics[j].CallIndex
180+
}
181+
return statics[i].Start < statics[j].Start
182+
})
183+
184+
if b.options.MergeAdjacentStatic {
185+
statics = mergeAdjacentStatics(statics)
186+
}
187+
188+
staticSections, err := b.encodeStaticSections(statics)
189+
if err != nil {
190+
return nil, nil, err
191+
}
192+
193+
signature, err := EncodeSignature(staticSections, repeats)
194+
if err != nil {
195+
return nil, nil, err
196+
}
197+
198+
return signature, &Plan{Static: staticSections, Repeat: repeats}, nil
199+
}
200+
201+
type SpanWithCall struct {
202+
CallIndex int
203+
Span
204+
}
205+
206+
func (b *Builder) encodeStaticSections(statics []SpanWithCall) ([]StaticSection, error) {
207+
var sections []StaticSection
208+
for _, s := range statics {
209+
if s.Len == 0 {
210+
continue
211+
}
212+
if s.CallIndex < 0 || s.CallIndex >= len(b.payload.Calls) {
213+
return nil, fmt.Errorf("tindex out of range: %d", s.CallIndex)
214+
}
215+
offset := s.Start
216+
length := s.Len
217+
for length > 0 {
218+
chunk := length
219+
if uint32(chunk) > b.options.MaxSize {
220+
chunk = int(b.options.MaxSize)
221+
}
222+
if uint32(offset) > b.options.MaxOffset {
223+
return nil, fmt.Errorf("cindex too large: %d", offset)
224+
}
225+
sections = append(sections, StaticSection{
226+
TIndex: uint8(s.CallIndex),
227+
CIndex: uint16(offset),
228+
Size: uint16(chunk),
229+
})
230+
offset += chunk
231+
length -= chunk
232+
}
233+
}
234+
return sections, nil
235+
}
236+
237+
func mergeSpans(spans []Span) []Span {
238+
if len(spans) == 0 {
239+
return nil
240+
}
241+
sort.Slice(spans, func(i, j int) bool { return spans[i].Start < spans[j].Start })
242+
out := []Span{spans[0]}
243+
for _, s := range spans[1:] {
244+
last := &out[len(out)-1]
245+
if s.Start <= last.Start+last.Len {
246+
end := max(last.Start+last.Len, s.Start+s.Len)
247+
last.Len = end - last.Start
248+
} else {
249+
out = append(out, s)
250+
}
251+
}
252+
return out
253+
}
254+
255+
func mergeAdjacentStatics(statics []SpanWithCall) []SpanWithCall {
256+
if len(statics) == 0 {
257+
return statics
258+
}
259+
out := []SpanWithCall{statics[0]}
260+
for _, s := range statics[1:] {
261+
last := &out[len(out)-1]
262+
if s.CallIndex == last.CallIndex && s.Start == last.Start+last.Len {
263+
last.Len += s.Len
264+
} else {
265+
out = append(out, s)
266+
}
267+
}
268+
return out
269+
}

0 commit comments

Comments
 (0)