Skip to content

Commit 18515fe

Browse files
committed
feat: implement Shasta anchor checkpoint decoding and add related tests
1 parent a75f806 commit 18515fe

File tree

3 files changed

+132
-8
lines changed

3 files changed

+132
-8
lines changed

internal/witness/abi.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package witness
22

33
import (
44
"errors"
5+
"fmt"
56
"math/big"
67
"reflect"
78
"strings"
@@ -145,6 +146,21 @@ func decodeAnchorV3ArgsSignalSlots(input []byte) ([][32]byte, error) {
145146
}
146147

147148
func decodeShastaAnchorCheckpoint(input []byte) (*ShastaCheckpoint, error) {
149+
// L2 Shasta anchor transaction (Anchor.anchorV4) encodes a single checkpoint struct:
150+
// (uint48 blockNumber, bytes32 blockHash, bytes32 stateRoot) -> 3 static 32-byte words.
151+
// This path avoids relying on the L1 ShastaAnchor ABI which includes dynamic fields.
152+
if len(input) == 96 {
153+
blockNumber := new(big.Int).SetBytes(input[:32])
154+
if blockNumber.BitLen() > 48 {
155+
return nil, fmt.Errorf("invalid shasta checkpoint block number: %#x", blockNumber)
156+
}
157+
return &ShastaCheckpoint{
158+
BlockNumber: blockNumber.Uint64(),
159+
BlockHash: common.BytesToHash(input[32:64]),
160+
StateRoot: common.BytesToHash(input[64:96]),
161+
}, nil
162+
}
163+
148164
if shastaAnchorV4Method.Name == "" {
149165
return nil, errors.New("shasta anchor ABI not initialized")
150166
}

internal/witness/batch_guest_input.go

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -246,22 +246,21 @@ func (g *BatchGuestInput) yieldShastaGuestInputs(yield func(*Pair) bool) {
246246
return d
247247
}()
248248

249-
var source manifest.DerivationSourceManifest
250-
decodeErr := rlp.DecodeBytes(decoded, &source)
249+
source, decodeErr := decodeShastaDerivationSourceManifest(decoded)
251250

252251
var validManifest *manifest.DerivationSourceManifest
253252

254253
if idx == len(g.Taiko.DataSources)-1 {
255254
// Normal source
256-
if decodeErr == nil && validateNormalProposalManifest(g, &source, g.Taiko.ProverData.LastAnchorBlockNumber) {
255+
if decodeErr == nil && validateNormalProposalManifest(g, source, g.Taiko.ProverData.LastAnchorBlockNumber) {
257256
if !validateShastaBlockBaseFee(g.Inputs, isFirstShastaProposal) {
258257
log.Warn("shasta block base fee is invalid, use default manifest")
259258
timestamp := clampTimestampLowerBound(lastParentBlockTimestamp, proposalTimestamp)
260259
coinbase := g.Taiko.BatchProposed.Proposer()
261260
anchorBlockNumber := g.Taiko.ProverData.LastAnchorBlockNumber
262261
validManifest = g.createDefaultManifest(timestamp, coinbase, anchorBlockNumber, lastParentBlockGasLimit)
263262
} else {
264-
validManifest = &source
263+
validManifest = source
265264
}
266265
} else {
267266
// Fallback
@@ -272,8 +271,8 @@ func (g *BatchGuestInput) yieldShastaGuestInputs(yield func(*Pair) bool) {
272271
}
273272
} else {
274273
// Force inclusion source
275-
if decodeErr == nil && validateForceIncProposalManifest(&source) {
276-
validManifest = &source
274+
if decodeErr == nil && validateForceIncProposalManifest(source) {
275+
validManifest = source
277276
if len(source.Blocks) > 0 {
278277
lastParentBlockTimestamp = source.Blocks[0].Timestamp
279278
lastParentBlockGasLimit = source.Blocks[0].GasLimit
@@ -344,6 +343,29 @@ func combineBlobData(blobs [][eth.BlobSize]byte) ([]byte, error) {
344343
return combined, nil
345344
}
346345

346+
type legacyDerivationSourceManifest struct {
347+
Blocks []*manifest.BlockManifest
348+
}
349+
350+
func decodeShastaDerivationSourceManifest(data []byte) (*manifest.DerivationSourceManifest, error) {
351+
// New format: `DerivationSourceManifest` is encoded as `[proverAuthBytes, blocks]`.
352+
var decoded manifest.DerivationSourceManifest
353+
if err := rlp.DecodeBytes(data, &decoded); err == nil {
354+
return &decoded, nil
355+
}
356+
357+
// Legacy format (used by current fixtures and raiko): `DerivationSourceManifest` is encoded as
358+
// `[blocks]`, without `proverAuthBytes`.
359+
var legacy legacyDerivationSourceManifest
360+
if err := rlp.DecodeBytes(data, &legacy); err != nil {
361+
return nil, err
362+
}
363+
return &manifest.DerivationSourceManifest{
364+
ProverAuthBytes: nil,
365+
Blocks: legacy.Blocks,
366+
}, nil
367+
}
368+
347369
func (g *BatchGuestInput) BlockProposed() BlockProposed {
348370
return g.Taiko.BatchProposed
349371
}
@@ -825,13 +847,17 @@ func validAnchorInNormalProposal(
825847
lastAnchorBlockNumber uint64,
826848
proposalBlockNumber uint64,
827849
) bool {
850+
// NOTE: align with raiko's Shasta rule: the maximum anchor can be `proposalBlockNumber - 1`.
851+
// See raiko `valid_anchor_in_normal_proposal` (ANCHOR_MIN_OFFSET = 1).
852+
const shastaAnchorMinOffset uint64 = 1
853+
828854
minAnchor := uint64(0)
829855
if proposalBlockNumber > manifest.AnchorMaxOffset {
830856
minAnchor = proposalBlockNumber - manifest.AnchorMaxOffset
831857
}
832858
maxAnchor := uint64(0)
833-
if proposalBlockNumber > manifest.AnchorMinOffset {
834-
maxAnchor = proposalBlockNumber - manifest.AnchorMinOffset
859+
if proposalBlockNumber > shastaAnchorMinOffset {
860+
maxAnchor = proposalBlockNumber - shastaAnchorMinOffset
835861
}
836862

837863
hasAnchorGrow := false
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package witness
2+
3+
import (
4+
"encoding/binary"
5+
"encoding/json"
6+
"testing"
7+
8+
"github.com/stretchr/testify/require"
9+
"github.com/taikoxyz/gaiko/tests/fixtures"
10+
"github.com/taikoxyz/taiko-mono/packages/taiko-client/pkg/utils"
11+
)
12+
13+
func TestShastaManifestMatchesInputBlockParams(t *testing.T) {
14+
payload, err := fixtures.ReadShastaFixture("input-52.json")
15+
require.NoError(t, err)
16+
17+
var input BatchGuestInput
18+
require.NoError(t, json.Unmarshal(payload, &input))
19+
require.Len(t, input.Inputs, 1)
20+
21+
shastaBlock, ok := input.Taiko.BatchProposed.(*ShastaBlockProposed)
22+
require.True(t, ok)
23+
eventData := shastaBlock.EventData()
24+
require.NotNil(t, eventData)
25+
require.Len(t, eventData.Proposal.Sources, 1)
26+
require.Len(t, input.Taiko.DataSources, 1)
27+
28+
combined, err := combineBlobData(input.Taiko.DataSources[0].TxDataFromBlob)
29+
require.NoError(t, err)
30+
31+
offset := int(eventData.Proposal.Sources[0].BlobSlice.Offset)
32+
require.GreaterOrEqual(t, len(combined), offset+64)
33+
34+
sizeBytes := combined[offset+32 : offset+64]
35+
size := binary.BigEndian.Uint64(sizeBytes[24:])
36+
end := offset + 64 + int(size)
37+
require.LessOrEqual(t, end, len(combined))
38+
39+
decoded, err := utils.Decompress(combined[offset+64 : end])
40+
require.NoError(t, err)
41+
42+
m, err := decodeShastaDerivationSourceManifest(decoded)
43+
require.NoError(t, err)
44+
require.Len(t, m.Blocks, 1)
45+
46+
require.True(t, validateInputBlockParam(m.Blocks[0], input.Inputs[0].Block))
47+
}
48+
49+
func TestShastaGuestInputsDoesNotFallbackToDefaultManifest(t *testing.T) {
50+
payload, err := fixtures.ReadShastaFixture("input-52.json")
51+
require.NoError(t, err)
52+
53+
var input BatchGuestInput
54+
require.NoError(t, json.Unmarshal(payload, &input))
55+
require.Len(t, input.Inputs, 1)
56+
57+
count := 0
58+
for range input.GuestInputs() {
59+
count++
60+
}
61+
require.Equal(t, len(input.Inputs), count)
62+
}
63+
64+
func TestShastaAnchorLinkageDecodesCheckpoint(t *testing.T) {
65+
payload, err := fixtures.ReadShastaFixture("input-52.json")
66+
require.NoError(t, err)
67+
68+
var input BatchGuestInput
69+
require.NoError(t, json.Unmarshal(payload, &input))
70+
require.Len(t, input.Inputs, 1)
71+
72+
shastaBlock, ok := input.Taiko.BatchProposed.(*ShastaBlockProposed)
73+
require.True(t, ok)
74+
eventData := shastaBlock.EventData()
75+
require.NotNil(t, eventData)
76+
77+
require.NoError(t, verifyShastaAnchorLinkage(
78+
input.Inputs,
79+
input.Taiko.L1AncestorHeaders,
80+
eventData.Proposal.OriginBlockHash,
81+
))
82+
}

0 commit comments

Comments
 (0)