diff --git a/Dockerfile b/Dockerfile index 2664071..0c8f46d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.26.5-alpine AS builder +FROM golang:1.27rc2-alpine AS builder RUN apk add make ARG BUILD_VERSION ENV BUILD_VERSION=${BUILD_VERSION} diff --git a/internal/tezos/prepare_transaction.go b/internal/tezos/prepare_transaction.go index cfdb69f..040cbef 100644 --- a/internal/tezos/prepare_transaction.go +++ b/internal/tezos/prepare_transaction.go @@ -65,6 +65,12 @@ func (c *tezosConnector) estimateAndAssignTxCost(ctx context.Context, op *codec. verb = "forced" } limits := v.Limits() + if i >= len(costs) { + log.L(ctx).Debugf("OP#%03d: %s fee(%s)=%d gas_limit(%s)=%d storage_limit(%s)=%d (no simulation cost data)", + i, v.Kind(), verb, limits.Fee, verb, limits.GasLimit, verb, limits.StorageLimit, + ) + continue + } log.L(ctx).Debugf("OP#%03d: %s gas_used(sim)=%d storage_used(sim)=%d storage_burn(sim)=%d alloc_burn(sim)=%d fee(%s)=%d gas_limit(%s)=%d storage_limit(%s)=%d ", i, v.Kind(), costs[i].GasUsed, costs[i].StorageUsed, costs[i].StorageBurn, costs[i].AllocationBurn, verb, limits.Fee, verb, limits.GasLimit, verb, limits.StorageLimit, @@ -98,8 +104,7 @@ func (c *tezosConnector) prepareInputParams(ctx context.Context, req *ffcapi.Tra for i, p := range req.Params { if p != nil { - err := tezosParams.UnmarshalJSON([]byte(*p)) - if err != nil { + if err := unmarshalParameters([]byte(*p), &tezosParams); err != nil { return tezosParams, i18n.NewError(ctx, msgs.MsgUnmarshalParamFail, i, err) } } @@ -108,6 +113,42 @@ func (c *tezosConnector) prepareInputParams(ctx context.Context, req *ffcapi.Tra return tezosParams, nil } +// unmarshalParameters parses micheline.Parameters from JSON, replicating the +// logic of (*micheline.Parameters).UnmarshalJSON but avoiding infinite +// recursion under Go 1.27+. In Go 1.27, encoding/json/v2 resolves the method +// set of a pointer-alias's underlying type, so the original +// +// type alias *Parameters; json.Unmarshal(data, alias(p)) +// +// pattern inside (*Parameters).UnmarshalJSON calls itself recursively until +// the stack overflows. Using a plain struct alias breaks the method-set chain. +func unmarshalParameters(data []byte, p *micheline.Parameters) error { + if len(data) == 0 { + return nil + } + if data[0] == '[' { + // non-entrypoint calling convention: value only + return json.Unmarshal(data, &p.Value) + } + // entrypoint calling convention: {"entrypoint": "...", "value": {...}} + type paramsAlias struct { + Entrypoint string `json:"entrypoint"` + Value micheline.Prim `json:"value"` + } + var alias paramsAlias + if err := json.Unmarshal(data, &alias); err != nil { + return err + } + p.Entrypoint = alias.Entrypoint + p.Value = alias.Value + if p.Value.IsValid() { + return nil + } + // legacy calling convention: bare prim value without entrypoint wrapper + p.Entrypoint = "default" + return json.Unmarshal(data, &p.Value) +} + func (c *tezosConnector) buildOp(ctx context.Context, params micheline.Parameters, fromString, toString string, nonce *fftypes.FFBigInt) (*codec.Op, error) { op := codec.NewOp() diff --git a/internal/tezos/prepare_transaction_test.go b/internal/tezos/prepare_transaction_test.go index 043094b..a0b82c2 100644 --- a/internal/tezos/prepare_transaction_test.go +++ b/internal/tezos/prepare_transaction_test.go @@ -421,6 +421,43 @@ func TestTransactionPrepareWithRevealEmptyServerError(t *testing.T) { assert.Nil(t, resp) } +func Test_estimateAndAssignTxCostFewerSimResultsThanOpContents(t *testing.T) { + ctx, c, mRPC, done := newTestConnector(t) + defer done() + + // Simulate returns only one result, but the op has two contents entries. + // The bounds check should prevent an index-out-of-bounds panic for the second entry. + mRPC.On("Simulate", ctx, mock.Anything, mock.Anything). + Return(&rpc.Receipt{ + Op: &rpc.Operation{ + Contents: []rpc.TypedOperation{ + rpc.Transaction{ + Manager: rpc.Manager{ + Generic: rpc.Generic{ + Metadata: rpc.OperationMetadata{ + Result: rpc.OperationResult{ + Status: tezos.OpStatusApplied, + }, + }, + }, + }, + }, + }, + }, + }, nil) + + op := codec.NewOp() + txArgs := contract.TxArgs{} + op.WithContents(txArgs.Encode()) + op.WithContents(txArgs.Encode()) // second entry has no corresponding simulation cost + + opts := &rpc.DefaultOptions + opts.IgnoreLimits = true + + _, err := c.estimateAndAssignTxCost(ctx, op, opts) + assert.NoError(t, err) +} + func Test_getNetworkParamsByName(t *testing.T) { params := getNetworkParamsByName("ghostnet") assert.Equal(t, params, tezos.GhostnetParams)