Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions core/services/pipeline/getters.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ package pipeline
import (
"bytes"
"encoding/json"
stderrors "errors"
"fmt"
"io"
"strings"
"time"

Expand Down Expand Up @@ -133,6 +135,15 @@ func JSONWithVarExprs(jsExpr string, vars Vars, allowErrors bool) GetterFunc {
if err := jd.Decode(&val); err != nil {
return nil, errors.Wrapf(ErrBadInput, "while unmarshalling JSON: %v; js: %s", err, string(replaced))
}
// Reject inputs the decoder only partially consumed. json.Decoder reads a
// single JSON value from the stream and silently ignores trailing bytes,
// so an Ethereum address like "0x8829..." decodes as the JSON number 0
// with the rest discarded. We signal ErrParameterEmpty (not ErrBadInput)
// so ResolveParam falls through to the next getter (e.g. NonemptyString),
// letting a literal address be handled as intended. See issue #21768.
if _, err := jd.Token(); !stderrors.Is(err, io.EOF) {
return nil, errors.Wrapf(ErrParameterEmpty, "input is not a single JSON value; js: %s", string(replaced))
}
reinterpreted, err := jsonserializable.ReinterpretJSONNumbers(val)
if err != nil {
return nil, errors.Wrapf(ErrBadInput, "while processing json.Number: %v; js: %s", err, string(replaced))
Expand Down
11 changes: 11 additions & 0 deletions core/services/pipeline/getters_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,17 @@ func TestGetters_JSONWithVarExprs(t *testing.T) {
{`{ "$(foo.bar)": $(zet) }`, "value", 123, pipeline.ErrBadInput, false},
{`{ "x": { "__chainlink_key_path__": 0 } }`, "", nil, pipeline.ErrBadInput, false},
{`{ "e": $(err)`, "e", nil, pipeline.ErrBadInput, false},
// #21768: inputs the JSON decoder only partially consumes must be
// rejected as ErrParameterEmpty so ResolveParam falls through to the
// next getter (e.g. NonemptyString). A literal Ethereum address reads
// as the JSON number 0 with the trailing characters silently dropped.
{`0x52908400098527886E0F7030069857D2E4169EE7`, "", nil, pipeline.ErrParameterEmpty, false},
{`0xdeadbeef`, "", nil, pipeline.ErrParameterEmpty, false},
{`123abc`, "", nil, pipeline.ErrParameterEmpty, false},
{`0 0`, "", nil, pipeline.ErrParameterEmpty, false},
{`{"a":1} extra`, "", nil, pipeline.ErrParameterEmpty, false},
{`[1,2] junk`, "", nil, pipeline.ErrParameterEmpty, false},
{`true false`, "", nil, pipeline.ErrParameterEmpty, false},
}

for _, test := range tests {
Expand Down
44 changes: 44 additions & 0 deletions core/services/pipeline/task.eth_tx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,50 @@ func TestETHTxTask(t *testing.T) {
},
nil, nil, "", pipeline.RunInfo{},
},
{
// #21768: a literal address in `from` (not wrapped in a JSON array)
// used to fail with "AddressSliceParam: cannot convert int64".
"happy (literal from address, #21768)",
`0x882969652440ccf14a5dbb9bd53eb21cb1e11e5c`,
"0xDeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF",
"foobar",
"12345",
`{ "jobID": 321, "requestID": "0x5198616554d738d9485d1a7cf53b2f33e09c3bbc8fe9ac0020bd672cd2bc15d2", "requestTxHash": "0xc524fafafcaec40652b1f84fca09c231185437d008d195fccf2f51e64b7062f8" }`,
`0`,
testutils.FixtureChainID.String(),
`{"CheckerType": "vrf_v2", "VRFCoordinatorAddress": "0x2E396ecbc8223Ebc16EC45136228AE5EDB649943"}`,
nil,
false,
pipeline.NewVarsFrom(nil),
nil,
func(keyStore *keystoremocks.Eth, txManager *txmmocks.MockEvmTxManager) {
data := []byte("foobar")
gasLimit := uint64(12345)
jobID := int32(321)
addr := common.HexToAddress("0x2E396ecbc8223Ebc16EC45136228AE5EDB649943")
txMeta := &txmgr.TxMeta{
JobID: &jobID,
RequestID: &reqID,
RequestTxHash: &reqTxHash,
FailOnRevert: null.BoolFrom(false),
}
keyStore.On("GetRoundRobinAddress", mock.Anything, testutils.FixtureChainID, from).Return(from, nil)
txManager.On("CreateTransaction", mock.Anything, txmgr.TxRequest{
FromAddress: from,
ToAddress: to,
EncodedPayload: data,
FeeLimit: gasLimit,
Meta: txMeta,
Strategy: txmgrcommon.NewSendEveryStrategy(),
Checker: txmgr.TransmitCheckerSpec{
CheckerType: txmgr.TransmitCheckerTypeVRFV2,
VRFCoordinatorAddress: &addr,
},
SignalCallback: true,
}).Return(txmgr.Tx{}, nil)
},
nil, nil, "", pipeline.RunInfo{},
},
{
"happy (with vars)",
`[ $(fromAddr) ]`,
Expand Down
16 changes: 9 additions & 7 deletions core/services/pipeline/task_params.go
Original file line number Diff line number Diff line change
Expand Up @@ -666,14 +666,16 @@ func (s *AddressSliceParam) UnmarshalPipelineParam(val any) error {
case []common.Address:
asp = v
case string:
err := json.Unmarshal([]byte(v), &asp)
if err != nil {
return errors.Wrapf(ErrBadInput, "AddressSliceParam: %v", err)
}
return s.UnmarshalPipelineParam([]byte(v))
case []byte:
err := json.Unmarshal(v, &asp)
if err != nil {
return errors.Wrapf(ErrBadInput, "AddressSliceParam: %v", err)
if err := json.Unmarshal(v, &asp); err != nil {
// Not a JSON array. Fall back to a single literal address, e.g.
// `from="0x8829..."`, which is not valid JSON on its own. See #21768.
var addr AddressParam
if aerr := addr.UnmarshalPipelineParam(v); aerr != nil {
return errors.Wrapf(ErrBadInput, "AddressSliceParam: %v", err)
}
asp = append(asp, common.Address(addr))
}
case []any:
for _, a := range v {
Expand Down
48 changes: 48 additions & 0 deletions core/services/pipeline/task_params_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,8 +178,13 @@ func TestAddressSliceParam_UnmarshalPipelineParam(t *testing.T) {
{"[]interface{} with []byte", []any{[]byte(addr1.String()), []byte(addr2.String())}, expected, nil},
{"nil", nil, pipeline.AddressSliceParam(nil), nil},

// #21768: a single literal address (not a JSON array) must be accepted.
{"single literal address string", "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", pipeline.AddressSliceParam{addr1}, nil},
{"single literal address []byte", []byte("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"), pipeline.AddressSliceParam{addr1}, nil},

{"bad json", `[ "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" "0xcafebabecafebabecafebabecafebabecafebabe" ]`, nil, pipeline.ErrBadInput},
{"[]interface{} with bad types", []any{123, true}, nil, pipeline.ErrBadInput},
{"garbage string", "not-an-address", nil, pipeline.ErrBadInput},
}

for _, test := range tests {
Expand All @@ -194,6 +199,49 @@ func TestAddressSliceParam_UnmarshalPipelineParam(t *testing.T) {
}
}

// TestAddressSliceParam_ResolveFromGetterChain_Issue21768 drives the exact
// getter chain the ethtx task uses to resolve its `from` field (see
// task.eth_tx.go: From(VarExpr, JSONWithVarExprs, NonemptyString, nil)). It
// uses the real exported resolver functions with no mocks, DB, or stubs, so it
// pins down the #21768 regression at the layer where the bug actually lived:
// a literal address like "0x8829..." was decoded by json.Decoder as the number
// 0, surfaced as int64(0), and rejected downstream as "cannot convert int64".
func TestAddressSliceParam_ResolveFromGetterChain_Issue21768(t *testing.T) {
t.Parallel()

addr := common.HexToAddress("0x882969652440ccf14a5dbb9bd53eb21cb1e11e5c")
vars := pipeline.NewVarsFrom(map[string]any{
"fromAddr": addr,
"fromAddrs": []common.Address{addr},
})

tests := []struct {
name string
from string
expected pipeline.AddressSliceParam
}{
{"literal address (regression #21768)", "0x882969652440ccf14a5dbb9bd53eb21cb1e11e5c", pipeline.AddressSliceParam{addr}},
{"literal address with surrounding spaces", " 0x882969652440ccf14a5dbb9bd53eb21cb1e11e5c ", pipeline.AddressSliceParam{addr}},
{"json array (already worked)", `[ "0x882969652440ccf14a5dbb9bd53eb21cb1e11e5c" ]`, pipeline.AddressSliceParam{addr}},
{"json array with var expr", `[ $(fromAddr) ]`, pipeline.AddressSliceParam{addr}},
{"variable expression slice", "$(fromAddrs)", pipeline.AddressSliceParam{addr}},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
var fromAddrs pipeline.AddressSliceParam
err := pipeline.ResolveParam(&fromAddrs, pipeline.From(
pipeline.VarExpr(test.from, vars),
pipeline.JSONWithVarExprs(test.from, vars, false),
pipeline.NonemptyString(test.from),
nil,
))
require.NoError(t, err)
require.Equal(t, test.expected, fromAddrs)
})
}
}

func TestUint64Param_UnmarshalPipelineParam(t *testing.T) {
t.Parallel()

Expand Down