From 186289ac72e3bb50882649582f468ea93ec95fe0 Mon Sep 17 00:00:00 2001 From: Connor Stein Date: Wed, 19 May 2021 12:14:31 -0400 Subject: [PATCH] Feature/ch7211: VRF jpv2 crud (#4422) VRF jpv2 crud --- core/adapters/random.go | 18 +- core/adapters/random_test.go | 3 +- core/cmd/jobs_commands.go | 8 + core/cmd/jobs_commands_test.go | 24 ++ core/cmd/local_client_vrf.go | 21 +- core/cmd/local_client_vrf_test.go | 12 +- core/internal/cltest/vrf.go | 9 +- core/internal/cltest/vrf/vrf.go | 24 -- core/services/chainlink/application.go | 3 + core/services/job/models.go | 14 + core/services/job/orm.go | 38 +-- core/services/pipeline/common.go | 3 + core/services/pipeline/task.vrf.go | 24 ++ .../signatures/secp256k1}/public_key.go | 44 ++- core/services/vrf/delegate.go | 31 ++ .../models/vrfkey => services/vrf}/doc.go | 2 +- core/services/vrf/models.go | 76 +++++ core/services/vrf/orm.go | 50 +++ .../vrfkey => services/vrf}/private_key.go | 82 ++++- .../vrf}/private_key_test.go | 15 +- .../vrf}/public_key_test.go | 10 +- core/services/vrf/seed.go | 14 + .../vrf}/serialization_test.go | 8 +- core/services/vrf/validate.go | 55 +++ core/services/vrf/validate_test.go | 78 +++++ core/{store => services/vrf}/vrf_key_store.go | 40 +-- .../vrf}/vrf_key_store_db.go | 52 +-- .../vrf}/vrf_key_store_test.go | 14 +- .../vrf/vrf_simulate_blockchain_test.go | 11 +- core/store/migrations/0028_vrf_v2.go | 43 +++ .../models/vrf_coordinator_interface_test.go | 8 +- core/store/models/vrfkey/serialization.go | 166 --------- core/store/orm/orm.go | 30 -- core/store/store.go | 6 +- core/testdata/testspecs/v2_specs.go | 119 +++++++ core/web/jobs_controller.go | 4 + core/web/jobs_controller_test.go | 316 ++++++++---------- core/web/presenters/job.go | 35 +- core/web/presenters/job_test.go | 6 + 39 files changed, 969 insertions(+), 547 deletions(-) delete mode 100644 core/internal/cltest/vrf/vrf.go create mode 100644 core/services/pipeline/task.vrf.go rename core/{store/models/vrfkey => services/signatures/secp256k1}/public_key.go (72%) create mode 100644 core/services/vrf/delegate.go rename core/{store/models/vrfkey => services/vrf}/doc.go (99%) create mode 100644 core/services/vrf/models.go create mode 100644 core/services/vrf/orm.go rename core/{store/models/vrfkey => services/vrf}/private_key.go (55%) rename core/{store/models/vrfkey => services/vrf}/private_key_test.go (88%) rename core/{store/models/vrfkey => services/vrf}/public_key_test.go (80%) rename core/{store/models/vrfkey => services/vrf}/serialization_test.go (88%) create mode 100644 core/services/vrf/validate.go create mode 100644 core/services/vrf/validate_test.go rename core/{store => services/vrf}/vrf_key_store.go (68%) rename core/{store => services/vrf}/vrf_key_store_db.go (77%) rename core/{store => services/vrf}/vrf_key_store_test.go (93%) create mode 100644 core/store/migrations/0028_vrf_v2.go delete mode 100644 core/store/models/vrfkey/serialization.go create mode 100644 core/testdata/testspecs/v2_specs.go diff --git a/core/adapters/random.go b/core/adapters/random.go index 00810b2de4c..063d2c8b2ed 100644 --- a/core/adapters/random.go +++ b/core/adapters/random.go @@ -4,10 +4,10 @@ import ( "fmt" "github.com/smartcontractkit/chainlink/core/internal/gethwrappers/generated/solidity_vrf_coordinator_interface" + "github.com/smartcontractkit/chainlink/core/services/signatures/secp256k1" "github.com/smartcontractkit/chainlink/core/services/vrf" "github.com/smartcontractkit/chainlink/core/store" "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/store/models/vrfkey" "github.com/smartcontractkit/chainlink/core/utils" "github.com/ethereum/go-ethereum/common" @@ -102,20 +102,20 @@ func (ra *Random) Perform(input models.RunInput, store *store.Store) models.RunO // getInputs parses the JSON input for the values needed by the random adapter, // or returns an error. func getInputs(ra *Random, input models.RunInput, store *store.Store) ( - vrfkey.PublicKey, vrf.PreSeedData, error) { + secp256k1.PublicKey, vrf.PreSeedData, error) { key, err := getKey(ra, input) if err != nil { - return vrfkey.PublicKey{}, vrf.PreSeedData{}, errors.Wrapf(err, + return secp256k1.PublicKey{}, vrf.PreSeedData{}, errors.Wrapf(err, "bad key for vrf task") } preSeed, err := getPreSeed(input) if err != nil { - return vrfkey.PublicKey{}, vrf.PreSeedData{}, errors.Wrap(err, + return secp256k1.PublicKey{}, vrf.PreSeedData{}, errors.Wrap(err, "bad seed for vrf task") } block, err := getBlockData(input) if err != nil { - return vrfkey.PublicKey{}, vrf.PreSeedData{}, err + return secp256k1.PublicKey{}, vrf.PreSeedData{}, err } s := vrf.PreSeedData{PreSeed: preSeed, BlockHash: block.hash, BlockNum: block.num} return key, s, nil @@ -175,7 +175,7 @@ func getPreSeed(input models.RunInput) (vrf.Seed, error) { return *rv, nil } -func checkKeyHash(key vrfkey.PublicKey, inputKeyHash []byte) error { +func checkKeyHash(key secp256k1.PublicKey, inputKeyHash []byte) error { keyHash, err := key.Hash() if err != nil { return errors.Wrapf(err, "could not compute %v' hash", key) @@ -188,11 +188,11 @@ func checkKeyHash(key vrfkey.PublicKey, inputKeyHash []byte) error { return nil } -var failedKey = vrfkey.PublicKey{} +var failedKey = secp256k1.PublicKey{} // getKey returns the public key for the VRF, or an error. -func getKey(ra *Random, input models.RunInput) (vrfkey.PublicKey, error) { - key, err := vrfkey.NewPublicKeyFromHex(ra.PublicKey) +func getKey(ra *Random, input models.RunInput) (secp256k1.PublicKey, error) { + key, err := secp256k1.NewPublicKeyFromHex(ra.PublicKey) if err != nil { return failedKey, errors.Wrapf(err, "could not parse %v as public key", ra.PublicKey) diff --git a/core/adapters/random_test.go b/core/adapters/random_test.go index 75bfc0072f5..cf662e9b2be 100644 --- a/core/adapters/random_test.go +++ b/core/adapters/random_test.go @@ -8,7 +8,6 @@ import ( "github.com/smartcontractkit/chainlink/core/adapters" "github.com/smartcontractkit/chainlink/core/internal/cltest" - tvrf "github.com/smartcontractkit/chainlink/core/internal/cltest/vrf" "github.com/smartcontractkit/chainlink/core/internal/gethwrappers/generated/solidity_vrf_coordinator_interface" "github.com/smartcontractkit/chainlink/core/internal/mocks" "github.com/smartcontractkit/chainlink/core/services/eth" @@ -55,7 +54,7 @@ func TestRandom_Perform(t *testing.T) { vrf.OnChainResponseLength, "wrong response length") response, err := vrf.UnmarshalProofResponse(onChainResponse) require.NoError(t, err, "random adapter produced bad proof response") - actualProof, err := response.CryptoProof(tvrf.SeedData(t, seed, hash, blockNum)) + actualProof, err := response.CryptoProof(vrf.TestXXXSeedData(t, seed, hash, blockNum)) require.NoError(t, err, "could not extract proof from random adapter response") expected := common.HexToHash( "0x71a7c50918feaa753485ae039cb84ddd70c5c85f66b236138dea453a23d0f27e") diff --git a/core/cmd/jobs_commands.go b/core/cmd/jobs_commands.go index e556576b100..994827482ec 100644 --- a/core/cmd/jobs_commands.go +++ b/core/cmd/jobs_commands.go @@ -106,6 +106,14 @@ func (p JobPresenter) FriendlyCreatedAt() string { if p.KeeperSpec != nil { return p.KeeperSpec.CreatedAt.Format(time.RFC3339) } + case presenters.CronJobSpec: + if p.CronSpec != nil { + return p.CronSpec.CreatedAt.Format(time.RFC3339) + } + case presenters.VRFJobSpec: + if p.VRFSpec != nil { + return p.VRFSpec.CreatedAt.Format(time.RFC3339) + } default: return "unknown" } diff --git a/core/cmd/jobs_commands_test.go b/core/cmd/jobs_commands_test.go index 77bd36424f1..712d918e072 100644 --- a/core/cmd/jobs_commands_test.go +++ b/core/cmd/jobs_commands_test.go @@ -172,6 +172,30 @@ func TestJob_FriendlyCreatedAt(t *testing.T) { }, now.Format(time.RFC3339), }, + { + "gets the cron spec created at timestamp", + &cmd.JobPresenter{ + JobResource: presenters.JobResource{ + Type: presenters.CronJobSpec, + CronSpec: &presenters.CronSpec{ + CreatedAt: now, + }, + }, + }, + now.Format(time.RFC3339), + }, + { + "gets the vrf spec created at timestamp", + &cmd.JobPresenter{ + JobResource: presenters.JobResource{ + Type: presenters.VRFJobSpec, + VRFSpec: &presenters.VRFSpec{ + CreatedAt: now, + }, + }, + }, + now.Format(time.RFC3339), + }, { "gets the off chain reporting spec created at timestamp", &cmd.JobPresenter{ diff --git a/core/cmd/local_client_vrf.go b/core/cmd/local_client_vrf.go index 90ab7388991..113a7067310 100644 --- a/core/cmd/local_client_vrf.go +++ b/core/cmd/local_client_vrf.go @@ -7,11 +7,12 @@ import ( "os" "time" + "github.com/smartcontractkit/chainlink/core/services/signatures/secp256k1" + "github.com/smartcontractkit/chainlink/core/services/vrf" + "github.com/pkg/errors" "github.com/smartcontractkit/chainlink/core/logger" - "github.com/smartcontractkit/chainlink/core/store" "github.com/smartcontractkit/chainlink/core/store/dialects" - "github.com/smartcontractkit/chainlink/core/store/models/vrfkey" "github.com/smartcontractkit/chainlink/core/utils" "github.com/urfave/cli" ) @@ -193,7 +194,7 @@ func (cli *Client) ImportVRFKey(c *cli.Context) error { } vrfKeyStore := app.GetStore().VRFKeyStore if err := vrfKeyStore.Import(keyjson, string(password)); err != nil { - if err == store.MatchingVRFKeyError { + if err == vrf.MatchingVRFKeyError { fmt.Println(`The database already has an entry for that public key.`) var key struct{ PublicKey string } if e := json.Unmarshal(keyjson, &key); e != nil { @@ -243,7 +244,7 @@ func (cli *Client) ExportVRFKey(c *cli.Context) error { } // getKeys retrieves the keys for an ExportVRFKey request -func getKeys(cli *Client, c *cli.Context) (*vrfkey.EncryptedVRFKey, error) { +func getKeys(cli *Client, c *cli.Context) (*vrf.EncryptedVRFKey, error) { publicKey, err := getPublicKey(c) if err != nil { return nil, err @@ -284,14 +285,14 @@ func (cli *Client) DeleteVRFKey(c *cli.Context) error { hardDelete := c.Bool("hard") if hardDelete { if err := vrfKeyStore.Delete(publicKey); err != nil { - if err == store.AttemptToDeleteNonExistentKeyFromDB { + if err == vrf.AttemptToDeleteNonExistentKeyFromDB { fmt.Printf("There is already no entry in the DB for %s\n", publicKey) } return err } } else { if err := vrfKeyStore.Archive(publicKey); err != nil { - if err == store.AttemptToDeleteNonExistentKeyFromDB { + if err == vrf.AttemptToDeleteNonExistentKeyFromDB { fmt.Printf("There is already no entry in the DB for %s\n", publicKey) } return err @@ -300,13 +301,13 @@ func (cli *Client) DeleteVRFKey(c *cli.Context) error { return nil } -func getPublicKey(c *cli.Context) (vrfkey.PublicKey, error) { +func getPublicKey(c *cli.Context) (secp256k1.PublicKey, error) { if c.String("publicKey") == "" { - return vrfkey.PublicKey{}, fmt.Errorf("must specify public key") + return secp256k1.PublicKey{}, fmt.Errorf("must specify public key") } - publicKey, err := vrfkey.NewPublicKeyFromHex(c.String("publicKey")) + publicKey, err := secp256k1.NewPublicKeyFromHex(c.String("publicKey")) if err != nil { - return vrfkey.PublicKey{}, errors.Wrap(err, "failed to parse public key") + return secp256k1.PublicKey{}, errors.Wrap(err, "failed to parse public key") } return publicKey, nil } diff --git a/core/cmd/local_client_vrf_test.go b/core/cmd/local_client_vrf_test.go index eef7793baa8..35f942c6d68 100644 --- a/core/cmd/local_client_vrf_test.go +++ b/core/cmd/local_client_vrf_test.go @@ -9,11 +9,13 @@ import ( "testing" "time" + "github.com/smartcontractkit/chainlink/core/services/signatures/secp256k1" + "github.com/smartcontractkit/chainlink/core/services/vrf" + "github.com/smartcontractkit/chainlink/core/cmd" "github.com/smartcontractkit/chainlink/core/internal/cltest" "github.com/smartcontractkit/chainlink/core/internal/mocks" "github.com/smartcontractkit/chainlink/core/store" - "github.com/smartcontractkit/chainlink/core/store/models/vrfkey" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/urfave/cli" @@ -174,11 +176,11 @@ func TestLocalClientVRF_ImportVRFKey(t *testing.T) { c = cli.NewContext(nil, set, nil) require.NoError(t, client.ImportVRFKey(c)) - keys := []vrfkey.EncryptedVRFKey{} + keys := []vrf.EncryptedVRFKey{} app.GetStore().DB.Find(&keys) assert.Len(t, keys, 1) - pubKey, err := vrfkey.NewPublicKeyFromHex(vrfPublicKey) + pubKey, err := secp256k1.NewPublicKeyFromHex(vrfPublicKey) require.NoError(t, err) assert.Equal(t, pubKey, keys[0].PublicKey) @@ -280,12 +282,12 @@ func TestLocalClientVRF_DeleteVRFKey(t *testing.T) { c = cli.NewContext(nil, set, nil) require.NoError(t, client.DeleteVRFKey(c)) - keys := []vrfkey.EncryptedVRFKey{} + keys := []vrf.EncryptedVRFKey{} app.GetStore().DB.Find(&keys) assert.Len(t, keys, 0) } -func requireVRFKeysCount(t *testing.T, store *store.Store, length int) []*vrfkey.PublicKey { +func requireVRFKeysCount(t *testing.T, store *store.Store, length int) []*secp256k1.PublicKey { keys, err := store.VRFKeyStore.ListKeys() require.NoError(t, err) require.Len(t, keys, length) diff --git a/core/internal/cltest/vrf.go b/core/internal/cltest/vrf.go index 537f646f22c..cc2212de3a7 100644 --- a/core/internal/cltest/vrf.go +++ b/core/internal/cltest/vrf.go @@ -5,16 +5,19 @@ import ( "strings" "testing" + "github.com/smartcontractkit/chainlink/core/services/signatures/secp256k1" + "github.com/smartcontractkit/chainlink/core/services/vrf" + "github.com/smartcontractkit/chainlink/core/utils" + "github.com/stretchr/testify/require" strpkg "github.com/smartcontractkit/chainlink/core/store" - "github.com/smartcontractkit/chainlink/core/store/models/vrfkey" ) // StoredVRFKey creates a VRFKeyStore on store, imports a known VRF key into it, // and returns the corresponding public key. -func StoredVRFKey(t *testing.T, store *strpkg.Store) *vrfkey.PublicKey { - store.VRFKeyStore = strpkg.NewVRFKeyStore(store) +func StoredVRFKey(t *testing.T, store *strpkg.Store) *secp256k1.PublicKey { + store.VRFKeyStore = vrf.NewVRFKeyStore(vrf.NewORM(store.DB), utils.GetScryptParams(store.Config)) keyFile, err := ioutil.ReadFile("../../tools/clroot/vrfkey.json") require.NoError(t, err) rawPassword, err := ioutil.ReadFile("../../tools/clroot/password.txt") diff --git a/core/internal/cltest/vrf/vrf.go b/core/internal/cltest/vrf/vrf.go deleted file mode 100644 index 8d55ee01878..00000000000 --- a/core/internal/cltest/vrf/vrf.go +++ /dev/null @@ -1,24 +0,0 @@ -package cltest - -import ( - "math/big" - "testing" - - "github.com/ethereum/go-ethereum/common" - "github.com/stretchr/testify/require" - - "github.com/smartcontractkit/chainlink/core/services/vrf" -) - -// SeedData returns the request data needed to construct/validate a VRF proof, -// modulo the key. -func SeedData(t *testing.T, preSeed *big.Int, blockHash common.Hash, - blockNum int) vrf.PreSeedData { - seedAsSeed, err := vrf.BigToSeed(big.NewInt(0x10)) - require.NoError(t, err, "seed %x out of range", 0x10) - return vrf.PreSeedData{ - PreSeed: seedAsSeed, - BlockNum: uint64(blockNum), - BlockHash: blockHash, - } -} diff --git a/core/services/chainlink/application.go b/core/services/chainlink/application.go index a0d51cd220f..f2f0f7a27a5 100644 --- a/core/services/chainlink/application.go +++ b/core/services/chainlink/application.go @@ -11,6 +11,8 @@ import ( "syscall" "time" + "github.com/smartcontractkit/chainlink/core/services/vrf" + "github.com/pkg/errors" "github.com/smartcontractkit/chainlink/core/services/fluxmonitorv2" "github.com/smartcontractkit/chainlink/core/services/gasupdater" @@ -240,6 +242,7 @@ func NewApplication(config *orm.Config, ethClient eth.Client, advisoryLocker pos config, ), job.Keeper: keeper.NewDelegate(store.DB, jobORM, pipelineRunner, store.EthClient, headBroadcaster, logBroadcaster, config), + job.VRF: vrf.NewDelegate(vrf.NewORM(store.DB), pipelineRunner, pipelineORM), } ) diff --git a/core/services/job/models.go b/core/services/job/models.go index ba718199971..f2a3496701e 100644 --- a/core/services/job/models.go +++ b/core/services/job/models.go @@ -5,6 +5,8 @@ import ( "strconv" "time" + "github.com/smartcontractkit/chainlink/core/services/signatures/secp256k1" + "github.com/ethereum/go-ethereum/common" "github.com/lib/pq" "github.com/smartcontractkit/chainlink/core/assets" @@ -21,6 +23,7 @@ const ( FluxMonitor Type = "fluxmonitor" OffchainReporting Type = "offchainreporting" Keeper Type = "keeper" + VRF Type = "vrf" ) type Job struct { @@ -35,6 +38,8 @@ type Job struct { FluxMonitorSpec *FluxMonitorSpec KeeperSpecID *int32 KeeperSpec *KeeperSpec + VRFSpecID *int32 + VRFSpec *VRFSpec PipelineSpecID int32 PipelineSpec *pipeline.Spec JobSpecErrors []SpecError `gorm:"foreignKey:JobID"` @@ -210,3 +215,12 @@ type KeeperSpec struct { CreatedAt time.Time `toml:"-"` UpdatedAt time.Time `toml:"-"` } + +type VRFSpec struct { + ID int32 + CoordinatorAddress models.EIP55Address `toml:"coordinatorAddress"` + PublicKey secp256k1.PublicKey `toml:"publicKey"` + Confirmations uint32 `toml:"confirmations"` + CreatedAt time.Time `toml:"-"` + UpdatedAt time.Time `toml:"-"` +} diff --git a/core/services/job/orm.go b/core/services/job/orm.go index 1ebec74dcf2..0f27c42d7ab 100644 --- a/core/services/job/orm.go +++ b/core/services/job/orm.go @@ -192,10 +192,16 @@ func (o *orm) CreateJob(ctx context.Context, jobSpec *Job, taskDAG pipeline.Task err := tx.Create(&jobSpec.OffchainreportingOracleSpec).Error pqErr, ok := err.(*pgconn.PgError) if err != nil && ok && pqErr.Code == "23503" { - if !jobSpec.OffchainreportingOracleSpec.IsBootstrapPeer { + if pqErr.ConstraintName == "offchainreporting_oracle_specs_p2p_peer_id_fkey" { + return errors.Wrapf(ErrNoSuchPeerID, "%v", jobSpec.OffchainreportingOracleSpec.P2PPeerID) + } + if jobSpec.OffchainreportingOracleSpec != nil && !jobSpec.OffchainreportingOracleSpec.IsBootstrapPeer { if pqErr.ConstraintName == "offchainreporting_oracle_specs_transmitter_address_fkey" { return errors.Wrapf(ErrNoSuchTransmitterAddress, "%v", jobSpec.OffchainreportingOracleSpec.TransmitterAddress) } + if pqErr.ConstraintName == "offchainreporting_oracle_specs_encrypted_ocr_key_bundle_id_fkey" { + return errors.Wrapf(ErrNoSuchKeyBundle, "%v", jobSpec.OffchainreportingOracleSpec.EncryptedOCRKeyBundleID) + } } } if err != nil { @@ -214,6 +220,12 @@ func (o *orm) CreateJob(ctx context.Context, jobSpec *Job, taskDAG pipeline.Task return errors.Wrap(err, "failed to create CronSpec for jobSpec") } jobSpec.CronSpecID = &jobSpec.CronSpec.ID + case VRF: + err := tx.Create(&jobSpec.VRFSpec).Error + if err != nil { + return errors.Wrap(err, "failed to create CronSpec for jobSpec") + } + jobSpec.VRFSpecID = &jobSpec.VRFSpec.ID default: logger.Fatalf("Unsupported jobSpec.Type: %v", jobSpec.Type) } @@ -223,28 +235,7 @@ func (o *orm) CreateJob(ctx context.Context, jobSpec *Job, taskDAG pipeline.Task return errors.Wrap(err, "failed to create pipeline spec") } jobSpec.PipelineSpecID = pipelineSpecID - - if jobSpec.DirectRequestSpec != nil { - err = tx.FirstOrCreate(&jobSpec.DirectRequestSpec).Error - if err != nil { - return errors.Wrap(err, "error creating direct request spec") - } - jobSpec.DirectRequestSpecID = &jobSpec.DirectRequestSpec.ID - } - - err = tx.Omit("DirectRequestSpec").Create(jobSpec).Error - pqErr, ok := err.(*pgconn.PgError) - if err != nil && ok && pqErr.Code == "23503" { - if pqErr.ConstraintName == "offchainreporting_oracle_specs_p2p_peer_id_fkey" { - return errors.Wrapf(ErrNoSuchPeerID, "%v", jobSpec.OffchainreportingOracleSpec.P2PPeerID) - } - if jobSpec.OffchainreportingOracleSpec != nil && !jobSpec.OffchainreportingOracleSpec.IsBootstrapPeer { - if pqErr.ConstraintName == "offchainreporting_oracle_specs_encrypted_ocr_key_bundle_id_fkey" { - return errors.Wrapf(ErrNoSuchKeyBundle, "%v", jobSpec.OffchainreportingOracleSpec.EncryptedOCRKeyBundleID) - } - } - } - return errors.Wrap(err, "failed to create job") + return errors.Wrap(tx.Create(jobSpec).Error, "failed to create job") }) } @@ -392,6 +383,7 @@ func (o *orm) FindJob(id int32) (Job, error) { Preload("JobSpecErrors"). Preload("KeeperSpec"). Preload("CronSpec"). + Preload("VRFSpec"). First(&job, "jobs.id = ?", id). Error if job.OffchainreportingOracleSpec != nil { diff --git a/core/services/pipeline/common.go b/core/services/pipeline/common.go index 63140000161..04ced5932b8 100644 --- a/core/services/pipeline/common.go +++ b/core/services/pipeline/common.go @@ -231,6 +231,7 @@ const ( TaskTypeMultiply TaskType = "multiply" TaskTypeJSONParse TaskType = "jsonparse" TaskTypeAny TaskType = "any" + TaskTypeVRF TaskType = "vrf" // Testing only. TaskTypePanic TaskType = "panic" @@ -263,6 +264,8 @@ func UnmarshalTaskFromMap(taskType TaskType, taskMap interface{}, dotID string, task = &JSONParseTask{BaseTask: BaseTask{dotID: dotID, nPreds: nPreds}} case TaskTypeMultiply: task = &MultiplyTask{BaseTask: BaseTask{dotID: dotID, nPreds: nPreds}} + case TaskTypeVRF: + task = &VRFTask{BaseTask: BaseTask{dotID: dotID, nPreds: nPreds}} default: return nil, errors.Errorf(`unknown task type: "%v"`, taskType) } diff --git a/core/services/pipeline/task.vrf.go b/core/services/pipeline/task.vrf.go new file mode 100644 index 00000000000..085822e0384 --- /dev/null +++ b/core/services/pipeline/task.vrf.go @@ -0,0 +1,24 @@ +package pipeline + +import ( + "context" +) + +type VRFTask struct { + BaseTask `mapstructure:",squash"` +} + +var _ Task = (*VRFTask)(nil) + +func (t *VRFTask) Type() TaskType { + return TaskTypeVRF +} + +func (t *VRFTask) SetDefaults(inputValues map[string]string, g TaskDAG, self TaskDAGNode) error { + return nil +} + +func (t *VRFTask) Run(_ context.Context, _ JSONSerializable, inputs []Result) (result Result) { + // TODO + return Result{} +} diff --git a/core/store/models/vrfkey/public_key.go b/core/services/signatures/secp256k1/public_key.go similarity index 72% rename from core/store/models/vrfkey/public_key.go rename to core/services/signatures/secp256k1/public_key.go index 80678ca55cb..0d7557c09f2 100644 --- a/core/store/models/vrfkey/public_key.go +++ b/core/services/signatures/secp256k1/public_key.go @@ -1,13 +1,15 @@ -package vrfkey +package secp256k1 import ( + "database/sql/driver" "fmt" + "github.com/pkg/errors" + "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "go.dedis.ch/kyber/v3" - "github.com/smartcontractkit/chainlink/core/services/signatures/secp256k1" "github.com/smartcontractkit/chainlink/core/utils" ) @@ -19,7 +21,7 @@ type PublicKey [CompressedPublicKeyLength]byte const CompressedPublicKeyLength = 33 func init() { - if CompressedPublicKeyLength != (&secp256k1.Secp256k1{}).Point().MarshalSize() { + if CompressedPublicKeyLength != (&Secp256k1{}).Point().MarshalSize() { panic("disparity in expected public key lengths") } } @@ -33,7 +35,7 @@ func (k *PublicKey) Set(l PublicKey) { // Point returns the secp256k1 point corresponding to k func (k *PublicKey) Point() (kyber.Point, error) { - p := (&secp256k1.Secp256k1{}).Point() + p := (&Secp256k1{}).Point() return p, p.UnmarshalBinary(k[:]) } @@ -82,7 +84,7 @@ func (k *PublicKey) StringUncompressed() (string, error) { if err != nil { return "", err } - return hexutil.Encode(secp256k1.LongMarshal(p)), nil + return hexutil.Encode(LongMarshal(p)), nil } // Hash returns the solidity Keccak256 hash of k. Corresponds to hashOfKey on @@ -92,7 +94,7 @@ func (k *PublicKey) Hash() (common.Hash, error) { if err != nil { return common.Hash{}, err } - return utils.MustHash(string(secp256k1.LongMarshal(p))), nil + return utils.MustHash(string(LongMarshal(p))), nil } // MustHash is like Hash, but panics on error. Useful for testing. @@ -117,3 +119,33 @@ func (k *PublicKey) Address() common.Address { func (k *PublicKey) IsZero() bool { return *k == PublicKey{} } + +// MarshalText renders k as a text string +func (k PublicKey) MarshalText() ([]byte, error) { + return []byte(k.String()), nil +} + +// UnmarshalText reads a PublicKey into k from text, or errors +func (k *PublicKey) UnmarshalText(text []byte) error { + if err := k.SetFromHex(string(text)); err != nil { + return errors.Wrapf(err, "while parsing %s as public key", text) + } + return nil +} + +// Value marshals PublicKey to be saved in the DB +func (k PublicKey) Value() (driver.Value, error) { + return k.String(), nil +} + +// Scan reconstructs a PublicKey from a DB record of it. +func (k *PublicKey) Scan(value interface{}) error { + rawKey, ok := value.(string) + if !ok { + return errors.Wrap(fmt.Errorf("unable to convert %+v of type %T to PublicKey", value, value), "scan failure") + } + if err := k.SetFromHex(rawKey); err != nil { + return errors.Wrapf(err, "while scanning %s as PublicKey", rawKey) + } + return nil +} diff --git a/core/services/vrf/delegate.go b/core/services/vrf/delegate.go new file mode 100644 index 00000000000..94d5dceebce --- /dev/null +++ b/core/services/vrf/delegate.go @@ -0,0 +1,31 @@ +package vrf + +import ( + "github.com/smartcontractkit/chainlink/core/services/job" + "github.com/smartcontractkit/chainlink/core/services/pipeline" +) + +type Delegate struct { + vorm ORM + pr pipeline.Runner + porm pipeline.ORM +} + +func NewDelegate(vorm ORM, pr pipeline.Runner, porm pipeline.ORM) *Delegate { + return &Delegate{ + vorm: vorm, + pr: pr, + porm: porm, + } +} + +// JobType implements the job.Delegate interface +func (d *Delegate) JobType() job.Type { + return job.VRF +} + +// ServicesForSpec returns the flux monitor service for the job spec +func (d *Delegate) ServicesForSpec(spec job.Job) ([]job.Service, error) { + // TODO + return []job.Service{}, nil +} diff --git a/core/store/models/vrfkey/doc.go b/core/services/vrf/doc.go similarity index 99% rename from core/store/models/vrfkey/doc.go rename to core/services/vrf/doc.go index 26ba25eda29..acdd8cf996a 100644 --- a/core/store/models/vrfkey/doc.go +++ b/core/services/vrf/doc.go @@ -31,4 +31,4 @@ // on-chain verification mechanism in VRF.sol. If you want to know the VRF // output independently of the on-chain verification mechanism, you can get it // from vrf.UnmarshalSolidityProof(p).Output. -package vrfkey +package vrf diff --git a/core/services/vrf/models.go b/core/services/vrf/models.go new file mode 100644 index 00000000000..aaa36607358 --- /dev/null +++ b/core/services/vrf/models.go @@ -0,0 +1,76 @@ +package vrf + +import ( + "database/sql/driver" + "encoding/json" + "fmt" + "os" + "time" + + "github.com/smartcontractkit/chainlink/core/services/signatures/secp256k1" + + "github.com/ethereum/go-ethereum/accounts/keystore" + "github.com/pkg/errors" + "github.com/smartcontractkit/chainlink/core/utils" + "gorm.io/gorm" +) + +// EncryptedVRFKey contains encrypted private key to be serialized to DB +// +// We could re-use geth's key handling, here, but this makes it much harder to +// misuse VRF proving keys as ethereum keys or vice versa. +type EncryptedVRFKey struct { + PublicKey secp256k1.PublicKey `gorm:"primary_key"` + VRFKey gethKeyStruct `json:"vrf_key"` + CreatedAt time.Time `json:"-"` + UpdatedAt time.Time `json:"-"` + DeletedAt gorm.DeletedAt `json:"-"` +} + +// JSON returns the JSON representation of e, or errors +func (e *EncryptedVRFKey) JSON() ([]byte, error) { + keyJSON, err := json.Marshal(e) + if err != nil { + return nil, errors.Wrapf(err, "could not marshal encrypted key to JSON") + } + return keyJSON, nil +} + +// WriteToDisk writes the JSON representation of e to given file path, and +// ensures the file has appropriate access permissions +func (e *EncryptedVRFKey) WriteToDisk(path string) error { + keyJSON, err := e.JSON() + if err != nil { + return errors.Wrapf(err, "while marshaling key to save to %s", path) + } + userReadWriteOtherNoAccess := os.FileMode(0600) + return utils.WriteFileWithMaxPerms(path, keyJSON, userReadWriteOtherNoAccess) +} + +// Copied from go-ethereum/accounts/keystore/key.go's encryptedKeyJSONV3 +type gethKeyStruct struct { + Address string `json:"address"` + Crypto keystore.CryptoJSON `json:"crypto"` + Version int `json:"version"` +} + +func (k gethKeyStruct) Value() (driver.Value, error) { + return json.Marshal(&k) +} + +func (k *gethKeyStruct) Scan(value interface{}) error { + // With sqlite gorm driver, we get a []byte, here. With postgres, a string! + // https://github.com/jinzhu/gorm/issues/2276 + var toUnmarshal []byte + switch s := value.(type) { + case []byte: + toUnmarshal = s + case string: + toUnmarshal = []byte(s) + default: + return errors.Wrap( + fmt.Errorf("unable to convert %+v of type %T to gethKeyStruct", + value, value), "scan failure") + } + return json.Unmarshal(toUnmarshal, k) +} diff --git a/core/services/vrf/orm.go b/core/services/vrf/orm.go new file mode 100644 index 00000000000..218f05838db --- /dev/null +++ b/core/services/vrf/orm.go @@ -0,0 +1,50 @@ +package vrf + +import ( + "gorm.io/gorm" +) + +type ORM interface { + FirstOrCreateEncryptedSecretVRFKey(k *EncryptedVRFKey) error + ArchiveEncryptedSecretVRFKey(k *EncryptedVRFKey) error + DeleteEncryptedSecretVRFKey(k *EncryptedVRFKey) error + FindEncryptedSecretVRFKeys(where ...EncryptedVRFKey) ([]*EncryptedVRFKey, error) +} + +type orm struct { + db *gorm.DB +} + +var _ ORM = &orm{} + +func NewORM(db *gorm.DB) ORM { + return &orm{ + db: db, + } +} + +// FirstOrCreateEncryptedVRFKey returns the first key found or creates a new one in the orm. +func (orm *orm) FirstOrCreateEncryptedSecretVRFKey(k *EncryptedVRFKey) error { + return orm.db.FirstOrCreate(k).Error +} + +// ArchiveEncryptedVRFKey soft-deletes k from the encrypted keys table, or errors +func (orm *orm) ArchiveEncryptedSecretVRFKey(k *EncryptedVRFKey) error { + return orm.db.Delete(k).Error +} + +// DeleteEncryptedVRFKey deletes k from the encrypted keys table, or errors +func (orm *orm) DeleteEncryptedSecretVRFKey(k *EncryptedVRFKey) error { + return orm.db.Unscoped().Delete(k).Error +} + +// FindEncryptedVRFKeys retrieves matches to where from the encrypted keys table, or errors +func (orm *orm) FindEncryptedSecretVRFKeys(where ...EncryptedVRFKey) ( + retrieved []*EncryptedVRFKey, err error) { + var anonWhere []interface{} // Find needs "where" contents coerced to interface{} + for _, constraint := range where { + c := constraint + anonWhere = append(anonWhere, &c) + } + return retrieved, orm.db.Find(&retrieved, anonWhere...).Error +} diff --git a/core/store/models/vrfkey/private_key.go b/core/services/vrf/private_key.go similarity index 55% rename from core/store/models/vrfkey/private_key.go rename to core/services/vrf/private_key.go index 15dfd2502f5..14d204239a0 100644 --- a/core/store/models/vrfkey/private_key.go +++ b/core/services/vrf/private_key.go @@ -1,14 +1,17 @@ -package vrfkey +package vrf import ( "crypto/ecdsa" + "encoding/json" - "github.com/smartcontractkit/chainlink/core/services/signatures/secp256k1" - "github.com/smartcontractkit/chainlink/core/services/vrf" + "github.com/google/uuid" + "github.com/smartcontractkit/chainlink/core/utils" "fmt" "math/big" + "github.com/smartcontractkit/chainlink/core/services/signatures/secp256k1" + "github.com/ethereum/go-ethereum/accounts/keystore" "github.com/pkg/errors" "go.dedis.ch/kyber/v3" @@ -24,7 +27,7 @@ import ( // don't need to know the secret key explicitly. (See, e.g., MarshaledProof.) type PrivateKey struct { k kyber.Scalar - PublicKey PublicKey + PublicKey secp256k1.PublicKey } var suite = secp256k1.NewBlakeKeccackSecp256k1() @@ -42,10 +45,10 @@ func newPrivateKey(rawKey *big.Int) (*PrivateKey, error) { if err != nil { panic(errors.Wrapf(err, "could not marshal public key")) } - if len(pk) != CompressedPublicKeyLength { + if len(pk) != secp256k1.CompressedPublicKeyLength { panic(fmt.Errorf("public key %x has wrong length", pk)) } - if l := copy(sk.PublicKey[:], pk[:]); l != CompressedPublicKeyLength { + if l := copy(sk.PublicKey[:], pk[:]); l != secp256k1.CompressedPublicKeyLength { panic(fmt.Errorf("failed to copy correct length in serialized public key")) } return sk, nil @@ -53,9 +56,9 @@ func newPrivateKey(rawKey *big.Int) (*PrivateKey, error) { // MarshaledProof is a VRF proof of randomness using i.Key and seed, in the form // required by VRFCoordinator.sol's fulfillRandomnessRequest -func (k *PrivateKey) MarshaledProof(i vrf.PreSeedData) ( - vrf.MarshaledOnChainResponse, error) { - return vrf.GenerateProofResponse(secp256k1.ScalarToHash(k.k), i) +func (k *PrivateKey) MarshaledProof(i PreSeedData) ( + MarshaledOnChainResponse, error) { + return GenerateProofResponse(secp256k1.ScalarToHash(k.k), i) } // gethKey returns the geth keystore representation of k. Do not abuse this to @@ -75,7 +78,7 @@ func fromGethKey(gethKey *keystore.Key) *PrivateKey { if err != nil { panic(err) // Only way this can happen is out-of-memory failure } - var publicKey PublicKey + var publicKey secp256k1.PublicKey copy(publicKey[:], rawPublicKey) return &PrivateKey{secretKey, publicKey} } @@ -108,3 +111,62 @@ func (k *PrivateKey) String() string { func (k *PrivateKey) GoStringer() string { return k.String() } + +// passwordPrefix is added to the beginning of the passwords for +// EncryptedVRFKey's, so that VRF keys can't casually be used as ethereum +// keys, and vice-versa. If you want to do that, DON'T. +var passwordPrefix = "don't mix VRF and Ethereum keys!" + +func adulteratedPassword(auth string) string { + return passwordPrefix + auth +} + +// Encrypt returns the key encrypted with passphrase auth +func (k *PrivateKey) Encrypt(auth string, scryptParams utils.ScryptParams) (*EncryptedVRFKey, error) { + keyJSON, err := keystore.EncryptKey(k.gethKey(), adulteratedPassword(auth), + scryptParams.N, scryptParams.P) + if err != nil { + return nil, errors.Wrapf(err, "could not encrypt vrf key") + } + rv := EncryptedVRFKey{} + if e := json.Unmarshal(keyJSON, &rv.VRFKey); e != nil { + return nil, errors.Wrapf(e, "geth returned unexpected key material") + } + rv.PublicKey = k.PublicKey + roundTripKey, err := Decrypt(&rv, auth) + if err != nil { + return nil, errors.Wrapf(err, "could not decrypt just-encrypted key!") + } + if !roundTripKey.k.Equal(k.k) || roundTripKey.PublicKey != k.PublicKey { + panic(fmt.Errorf("roundtrip of key resulted in different value")) + } + return &rv, nil +} + +// Decrypt returns the PrivateKey in e, decrypted via auth, or an error +func Decrypt(e *EncryptedVRFKey, auth string) (*PrivateKey, error) { + // NOTE: We do this shuffle to an anonymous struct + // solely to add a a throwaway UUID, so we can leverage + // the keystore.DecryptKey from the geth which requires it + // as of 1.10.0. + keyJSON, err := json.Marshal(struct { + Address string `json:"address"` + Crypto keystore.CryptoJSON `json:"crypto"` + Version int `json:"version"` + Id string `json:"id"` + }{ + Address: e.VRFKey.Address, + Crypto: e.VRFKey.Crypto, + Version: e.VRFKey.Version, + Id: uuid.New().String(), + }) + if err != nil { + return nil, errors.Wrapf(err, "while marshaling key for decryption") + } + gethKey, err := keystore.DecryptKey(keyJSON, adulteratedPassword(auth)) + if err != nil { + return nil, errors.Wrapf(err, "could not decrypt key %s", + e.PublicKey.String()) + } + return fromGethKey(gethKey), nil +} diff --git a/core/store/models/vrfkey/private_key_test.go b/core/services/vrf/private_key_test.go similarity index 88% rename from core/store/models/vrfkey/private_key_test.go rename to core/services/vrf/private_key_test.go index feafe78f2ae..4ff14d2115c 100644 --- a/core/store/models/vrfkey/private_key_test.go +++ b/core/services/vrf/private_key_test.go @@ -1,4 +1,4 @@ -package vrfkey +package vrf import ( "encoding/hex" @@ -7,11 +7,9 @@ import ( "regexp" "testing" - "github.com/ethereum/go-ethereum/eth/ethconfig" + "github.com/smartcontractkit/chainlink/core/services/signatures/secp256k1" - tvrf "github.com/smartcontractkit/chainlink/core/internal/cltest/vrf" - "github.com/smartcontractkit/chainlink/core/internal/gethwrappers/generated/solidity_vrf_verifier_wrapper" - "github.com/smartcontractkit/chainlink/core/services/vrf" + "github.com/ethereum/go-ethereum/eth/ethconfig" "github.com/ethereum/go-ethereum/accounts/abi/bind" "github.com/ethereum/go-ethereum/accounts/abi/bind/backends" @@ -19,6 +17,7 @@ import ( "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/crypto" "github.com/pkg/errors" + "github.com/smartcontractkit/chainlink/core/internal/gethwrappers/generated/solidity_vrf_verifier_wrapper" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -27,7 +26,7 @@ var sk = 0xdeadbeefdeadbee var k = mustNewPrivateKey(big.NewInt(int64(sk))) var pkr = regexp.MustCompile(fmt.Sprintf( `PrivateKey\{k: , PublicKey: 0x[[:xdigit:]]{%d}\}`, - 2*CompressedPublicKeyLength)) + 2*secp256k1.CompressedPublicKeyLength)) func TestPrintingDoesNotLeakKey(t *testing.T) { v := fmt.Sprintf("%v", k) @@ -42,10 +41,10 @@ func TestMarshaledProof(t *testing.T) { blockHash := common.Hash{} blockNum := 0 preSeed := big.NewInt(1) - s := tvrf.SeedData(t, preSeed, blockHash, blockNum) + s := TestXXXSeedData(t, preSeed, blockHash, blockNum) proofResponse, err := k.MarshaledProof(s) require.NoError(t, err) - goProof, err := vrf.UnmarshalProofResponse(proofResponse) + goProof, err := UnmarshalProofResponse(proofResponse) require.NoError(t, err) actualProof, err := goProof.CryptoProof(s) require.NoError(t, err) diff --git a/core/store/models/vrfkey/public_key_test.go b/core/services/vrf/public_key_test.go similarity index 80% rename from core/store/models/vrfkey/public_key_test.go rename to core/services/vrf/public_key_test.go index 0eef66f8bcf..a5cd2dfe919 100644 --- a/core/store/models/vrfkey/public_key_test.go +++ b/core/services/vrf/public_key_test.go @@ -1,8 +1,10 @@ -package vrfkey +package vrf import ( "testing" + "github.com/smartcontractkit/chainlink/core/services/signatures/secp256k1" + "github.com/smartcontractkit/chainlink/core/services/signatures/cryptotest" "github.com/stretchr/testify/assert" @@ -13,11 +15,11 @@ func TestValueScanIdentityPointSet(t *testing.T) { randomStream := cryptotest.NewStream(t, 0) for i := 0; i < 10; i++ { p := suite.Point().Pick(randomStream) - var pk, nPk, nnPk PublicKey + var pk, nPk, nnPk secp256k1.PublicKey marshaledKey, err := p.MarshalBinary() require.NoError(t, err, "failed to marshal public key") require.Equal(t, copy(pk[:], marshaledKey), - CompressedPublicKeyLength, "failed to copy marshaled key to pk") + secp256k1.CompressedPublicKeyLength, "failed to copy marshaled key to pk") assert.NotEqual(t, pk, nPk, "equality test succeeds on different keys!") np, err := pk.Point() require.NoError(t, err, "failed to marshal public key") @@ -36,7 +38,7 @@ func TestValueScanIdentityPointSet(t *testing.T) { // Tests that PublicKey.Hash gives the same result as the VRFCoordinator's func TestHash(t *testing.T) { - pk, err := NewPublicKeyFromHex("0x9dc09a0f898f3b5e8047204e7ce7e44b587920932f08431e29c9bf6923b8450a01") + pk, err := secp256k1.NewPublicKeyFromHex("0x9dc09a0f898f3b5e8047204e7ce7e44b587920932f08431e29c9bf6923b8450a01") assert.NoError(t, err) assert.Equal(t, "0xc4406d555db624837188b91514a5f47e34d825d935ab887a35c06a3e7c41de69", pk.MustHash().String()) } diff --git a/core/services/vrf/seed.go b/core/services/vrf/seed.go index 6bfa10cfa00..821d8a05825 100644 --- a/core/services/vrf/seed.go +++ b/core/services/vrf/seed.go @@ -2,6 +2,9 @@ package vrf import ( "math/big" + "testing" + + "github.com/stretchr/testify/require" "github.com/ethereum/go-ethereum/common" "github.com/pkg/errors" @@ -51,3 +54,14 @@ func FinalSeed(s PreSeedData) (finalSeed *big.Int) { seedHashMsg := append(s.PreSeed[:], s.BlockHash.Bytes()...) return utils.MustHash(string(seedHashMsg)).Big() } + +func TestXXXSeedData(t *testing.T, preSeed *big.Int, blockHash common.Hash, + blockNum int) PreSeedData { + seedAsSeed, err := BigToSeed(big.NewInt(0x10)) + require.NoError(t, err, "seed %x out of range", 0x10) + return PreSeedData{ + PreSeed: seedAsSeed, + BlockNum: uint64(blockNum), + BlockHash: blockHash, + } +} diff --git a/core/store/models/vrfkey/serialization_test.go b/core/services/vrf/serialization_test.go similarity index 88% rename from core/store/models/vrfkey/serialization_test.go rename to core/services/vrf/serialization_test.go index fadbe1f55a1..e5e20bdbc9a 100644 --- a/core/store/models/vrfkey/serialization_test.go +++ b/core/services/vrf/serialization_test.go @@ -1,12 +1,14 @@ -package vrfkey +package vrf import ( "math/big" "testing" + "github.com/smartcontractkit/chainlink/core/services/signatures/secp256k1" + "github.com/stretchr/testify/require" + "github.com/smartcontractkit/chainlink/core/utils" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) var phrase = "as3r8phu82u9ru843cdi4298yf" @@ -24,7 +26,7 @@ func TestPublicKeyRoundTrip(t *testing.T) { pk := serialK.PublicKey serialized, err := pk.Value() require.NoError(t, err, "failed to serialize public key for db") - var npk PublicKey + var npk secp256k1.PublicKey require.NoError(t, npk.Scan(serialized), "could not deserialize serialized public key") assert.Equal(t, pk, npk, "should get same key back after Value/Scan roundtrip") diff --git a/core/services/vrf/validate.go b/core/services/vrf/validate.go new file mode 100644 index 00000000000..30594b42866 --- /dev/null +++ b/core/services/vrf/validate.go @@ -0,0 +1,55 @@ +package vrf + +import ( + "bytes" + + "github.com/pelletier/go-toml" + "github.com/pkg/errors" + "github.com/smartcontractkit/chainlink/core/services/job" + "github.com/smartcontractkit/chainlink/core/services/pipeline" + "github.com/smartcontractkit/chainlink/core/services/signatures/secp256k1" +) + +var ( + ErrKeyNotSet = errors.New("key not set") +) + +func ValidateVRFSpec(tomlString string) (job.Job, error) { + var jb = job.Job{Pipeline: *pipeline.NewTaskDAG()} + + tree, err := toml.Load(tomlString) + if err != nil { + return jb, errors.Wrap(err, "toml error on load") + } + + err = tree.Unmarshal(&jb) + if err != nil { + return jb, errors.Wrap(err, "toml unmarshal error on spec") + } + if jb.Type != job.VRF { + return jb, errors.Errorf("unsupported type %s", jb.Type) + } + if jb.SchemaVersion != uint32(1) { + return jb, errors.Errorf("the only supported schema version is currently 1, got %v", jb.SchemaVersion) + } + + var spec job.VRFSpec + err = tree.Unmarshal(&spec) + if err != nil { + return jb, errors.Wrap(err, "toml unmarshal error on job") + } + var empty secp256k1.PublicKey + if bytes.Equal(spec.PublicKey[:], empty[:]) { + return jb, errors.Wrap(ErrKeyNotSet, "publicKey") + } + if spec.Confirmations == 0 { + return jb, errors.Wrap(ErrKeyNotSet, "confirmations") + } + if spec.CoordinatorAddress.String() == "" { + return jb, errors.Wrap(ErrKeyNotSet, "coordinatorAddress") + } + + jb.VRFSpec = &spec + + return jb, nil +} diff --git a/core/services/vrf/validate_test.go b/core/services/vrf/validate_test.go new file mode 100644 index 00000000000..dbadc5bcabf --- /dev/null +++ b/core/services/vrf/validate_test.go @@ -0,0 +1,78 @@ +package vrf + +import ( + "testing" + + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" + + "github.com/smartcontractkit/chainlink/core/services/job" + "github.com/stretchr/testify/require" +) + +func TestValidateVRFJobSpec(t *testing.T) { + var tt = []struct { + name string + toml string + assertion func(t *testing.T, os job.Job, err error) + }{ + { + name: "valid spec", + toml: ` +type = "vrf" +schemaVersion = 1 +confirmations = 10 +publicKey = "0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F8179800" +coordinatorAddress = "0xB3b7874F13387D44a3398D298B075B7A3505D8d4" +observationSource = """ +getrandomvalue [type=vrf]; +""" +`, + assertion: func(t *testing.T, s job.Job, err error) { + require.NoError(t, err) + require.NotNil(t, s.VRFSpec) + assert.Equal(t, uint32(10), s.VRFSpec.Confirmations) + assert.Equal(t, "0xB3b7874F13387D44a3398D298B075B7A3505D8d4", s.VRFSpec.CoordinatorAddress.String()) + assert.Equal(t, "0x79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179800", s.VRFSpec.PublicKey.String()) + }, + }, + { + name: "missing pubkey", + toml: ` +type = "vrf" +schemaVersion = 1 +confirmations = 10 +coordinatorAddress = "0xB3b7874F13387D44a3398D298B075B7A3505D8d4" +observationSource = """ +getrandomvalue [type=vrf]; +""" +`, + assertion: func(t *testing.T, s job.Job, err error) { + require.Error(t, err) + require.True(t, ErrKeyNotSet == errors.Cause(err)) + }, + }, + { + name: "missing coordinator address", + toml: ` +type = "vrf" +schemaVersion = 1 +confirmations = 10 +publicKey = "0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F8179800" +observationSource = """ +getrandomvalue [type=vrf]; +""" +`, + assertion: func(t *testing.T, s job.Job, err error) { + require.Error(t, err) + require.True(t, ErrKeyNotSet == errors.Cause(err)) + }, + }, + } + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + s, err := ValidateVRFSpec(tc.toml) + tc.assertion(t, s, err) + }) + } +} diff --git a/core/store/vrf_key_store.go b/core/services/vrf/vrf_key_store.go similarity index 68% rename from core/store/vrf_key_store.go rename to core/services/vrf/vrf_key_store.go index db5a0b3ba2b..babf5bf58a6 100644 --- a/core/store/vrf_key_store.go +++ b/core/services/vrf/vrf_key_store.go @@ -1,14 +1,14 @@ -package store +package vrf import ( "fmt" "sync" + "github.com/smartcontractkit/chainlink/core/services/signatures/secp256k1" + "github.com/pkg/errors" "go.uber.org/multierr" - "github.com/smartcontractkit/chainlink/core/services/vrf" - "github.com/smartcontractkit/chainlink/core/store/models/vrfkey" "github.com/smartcontractkit/chainlink/core/utils" ) @@ -19,21 +19,23 @@ import ( // exposes VRF proof generation without the caller needing explicit knowledge of // the secret key. type VRFKeyStore struct { - lock sync.RWMutex - keys InMemoryKeyStore - store *Store + lock sync.RWMutex + keys InMemoryKeyStore + //store *store2.Store + orm ORM scryptParams utils.ScryptParams } -type InMemoryKeyStore = map[vrfkey.PublicKey]vrfkey.PrivateKey +type InMemoryKeyStore = map[secp256k1.PublicKey]PrivateKey // NewVRFKeyStore returns an empty VRFKeyStore -func NewVRFKeyStore(store *Store) *VRFKeyStore { +func NewVRFKeyStore(orm ORM, sp utils.ScryptParams) *VRFKeyStore { return &VRFKeyStore{ - lock: sync.RWMutex{}, - keys: make(InMemoryKeyStore), - store: store, - scryptParams: utils.GetScryptParams(store.Config), + lock: sync.RWMutex{}, + keys: make(InMemoryKeyStore), + orm: orm, + //store: store, + scryptParams: sp, } } @@ -42,13 +44,13 @@ func NewVRFKeyStore(store *Store) *VRFKeyStore { // // Key must have already been unlocked in ks, as constructing the VRF proof // requires the secret key. -func (ks *VRFKeyStore) GenerateProof(k vrfkey.PublicKey, i vrf.PreSeedData) ( - vrf.MarshaledOnChainResponse, error) { +func (ks *VRFKeyStore) GenerateProof(k secp256k1.PublicKey, i PreSeedData) ( + MarshaledOnChainResponse, error) { ks.lock.RLock() defer ks.lock.RUnlock() privateKey, found := ks.keys[k] if !found { - return vrf.MarshaledOnChainResponse{}, fmt.Errorf( + return MarshaledOnChainResponse{}, fmt.Errorf( "key %s has not been unlocked", k) } return privateKey.MarshaledProof(i) @@ -56,7 +58,7 @@ func (ks *VRFKeyStore) GenerateProof(k vrfkey.PublicKey, i vrf.PreSeedData) ( // Unlock tries to unlock each vrf key in the db, using the given pass phrase, // and returns any keys it manages to unlock, and any errors which result. -func (ks *VRFKeyStore) Unlock(phrase string) (keysUnlocked []vrfkey.PublicKey, +func (ks *VRFKeyStore) Unlock(phrase string) (keysUnlocked []secp256k1.PublicKey, merr error) { ks.lock.Lock() defer ks.lock.Unlock() @@ -65,7 +67,7 @@ func (ks *VRFKeyStore) Unlock(phrase string) (keysUnlocked []vrfkey.PublicKey, return nil, errors.Wrap(err, "while retrieving vrf keys from db") } for _, k := range keys { - key, err := k.Decrypt(phrase) + key, err := Decrypt(k, phrase) if err != nil { merr = multierr.Append(merr, err) continue @@ -78,7 +80,7 @@ func (ks *VRFKeyStore) Unlock(phrase string) (keysUnlocked []vrfkey.PublicKey, // Forget removes the in-memory copy of the secret key of k, or errors if not // present. Caller is responsible for taking ks.lock. -func (ks *VRFKeyStore) forget(k vrfkey.PublicKey) error { +func (ks *VRFKeyStore) forget(k secp256k1.PublicKey) error { if _, found := ks.keys[k]; !found { return fmt.Errorf("public key %s is not unlocked; can't forget it", k) } @@ -87,7 +89,7 @@ func (ks *VRFKeyStore) forget(k vrfkey.PublicKey) error { return nil } -func (ks *VRFKeyStore) Forget(k vrfkey.PublicKey) error { +func (ks *VRFKeyStore) Forget(k secp256k1.PublicKey) error { ks.lock.Lock() defer ks.lock.Unlock() return ks.forget(k) diff --git a/core/store/vrf_key_store_db.go b/core/services/vrf/vrf_key_store_db.go similarity index 77% rename from core/store/vrf_key_store_db.go rename to core/services/vrf/vrf_key_store_db.go index 91ccdd6badf..430fd2f8a84 100644 --- a/core/store/vrf_key_store_db.go +++ b/core/services/vrf/vrf_key_store_db.go @@ -1,23 +1,23 @@ -package store +package vrf import ( "encoding/json" "fmt" "github.com/pkg/errors" + "github.com/smartcontractkit/chainlink/core/services/signatures/secp256k1" "go.uber.org/multierr" - "github.com/smartcontractkit/chainlink/core/store/models/vrfkey" "github.com/smartcontractkit/chainlink/core/utils" ) // CreateKey returns a public key which is immediately unlocked in memory, and // saved in DB encrypted with phrase. If p is given, its parameters are used for // key derivation from the phrase. -func (ks *VRFKeyStore) CreateKey(phrase string) (vrfkey.PublicKey, error) { - key := vrfkey.CreateKey() +func (ks *VRFKeyStore) CreateKey(phrase string) (secp256k1.PublicKey, error) { + key := CreateKey() if err := ks.Store(key, phrase, ks.scryptParams); err != nil { - return vrfkey.PublicKey{}, err + return secp256k1.PublicKey{}, err } return key.PublicKey, nil } @@ -26,8 +26,8 @@ func (ks *VRFKeyStore) CreateKey(phrase string) (vrfkey.PublicKey, error) { // an encrypted key which is fast to unlock, but correspondingly easy to brute // force. It is not persisted to the DB, because no one should be keeping such // keys lying around. -func (ks *VRFKeyStore) CreateWeakInMemoryEncryptedKeyXXXTestingOnly(phrase string) (*vrfkey.EncryptedVRFKey, error) { - key := vrfkey.CreateKey() +func (ks *VRFKeyStore) CreateWeakInMemoryEncryptedKeyXXXTestingOnly(phrase string) (*EncryptedVRFKey, error) { + key := CreateKey() encrypted, err := key.Encrypt(phrase, utils.FastScryptParams) if err != nil { return nil, errors.Wrap(err, "while creating testing key") @@ -36,14 +36,14 @@ func (ks *VRFKeyStore) CreateWeakInMemoryEncryptedKeyXXXTestingOnly(phrase strin } // Store saves key to ks (in memory), and to the DB, encrypted with phrase -func (ks *VRFKeyStore) Store(key *vrfkey.PrivateKey, phrase string, scryptParams utils.ScryptParams) error { +func (ks *VRFKeyStore) Store(key *PrivateKey, phrase string, scryptParams utils.ScryptParams) error { ks.lock.Lock() defer ks.lock.Unlock() encrypted, err := key.Encrypt(phrase, scryptParams) if err != nil { return errors.Wrap(err, "failed to encrypt key") } - if err := ks.store.FirstOrCreateEncryptedSecretVRFKey(encrypted); err != nil { + if err := ks.orm.FirstOrCreateEncryptedSecretVRFKey(encrypted); err != nil { return errors.Wrap(err, "failed to save encrypted key to db") } ks.keys[key.PublicKey] = *key @@ -51,16 +51,16 @@ func (ks *VRFKeyStore) Store(key *vrfkey.PrivateKey, phrase string, scryptParams } // StoreInMemoryXXXTestingOnly memorizes key, only in in-memory store. -func (ks *VRFKeyStore) StoreInMemoryXXXTestingOnly(key *vrfkey.PrivateKey) { +func (ks *VRFKeyStore) StoreInMemoryXXXTestingOnly(key *PrivateKey) { ks.lock.Lock() defer ks.lock.Unlock() ks.keys[key.PublicKey] = *key } -var zeroPublicKey = vrfkey.PublicKey{} +var zeroPublicKey = secp256k1.PublicKey{} // Archive soft-deletes keys with this public key from the keystore and the DB, if present. -func (ks *VRFKeyStore) Archive(key vrfkey.PublicKey) (err error) { +func (ks *VRFKeyStore) Archive(key secp256k1.PublicKey) (err error) { ks.lock.Lock() defer ks.lock.Unlock() if key == zeroPublicKey { @@ -76,12 +76,12 @@ func (ks *VRFKeyStore) Archive(key vrfkey.PublicKey) (err error) { } else if len(matches) == 0 { return AttemptToDeleteNonExistentKeyFromDB } - err2 := ks.store.ORM.ArchiveEncryptedSecretVRFKey(&vrfkey.EncryptedVRFKey{PublicKey: key}) + err2 := ks.orm.ArchiveEncryptedSecretVRFKey(&EncryptedVRFKey{PublicKey: key}) return multierr.Append(err, err2) } // Delete removes keys with this public key from the keystore and the DB, if present. -func (ks *VRFKeyStore) Delete(key vrfkey.PublicKey) (err error) { +func (ks *VRFKeyStore) Delete(key secp256k1.PublicKey) (err error) { ks.lock.Lock() defer ks.lock.Unlock() if key == zeroPublicKey { @@ -99,7 +99,7 @@ func (ks *VRFKeyStore) Delete(key vrfkey.PublicKey) (err error) { if len(matches) == 0 { return AttemptToDeleteNonExistentKeyFromDB } - err2 := ks.store.ORM.DeleteEncryptedSecretVRFKey(&vrfkey.EncryptedVRFKey{PublicKey: key}) + err2 := ks.orm.DeleteEncryptedSecretVRFKey(&EncryptedVRFKey{PublicKey: key}) return multierr.Append(err, err2) } @@ -108,7 +108,7 @@ func (ks *VRFKeyStore) Delete(key vrfkey.PublicKey) (err error) { func (ks *VRFKeyStore) Import(keyjson []byte, auth string) error { ks.lock.Lock() defer ks.lock.Unlock() - enckey := &vrfkey.EncryptedVRFKey{} + enckey := &EncryptedVRFKey{} if err := json.Unmarshal(keyjson, enckey); err != nil { return fmt.Errorf("could not parse %s as EncryptedVRFKey json", keyjson) } @@ -119,13 +119,13 @@ func (ks *VRFKeyStore) Import(keyjson []byte, auth string) error { if len(extantMatchingKeys) != 0 { return MatchingVRFKeyError } - key, err := enckey.Decrypt(auth) + key, err := Decrypt(enckey, auth) if err != nil { return errors.Wrapf(err, "while attempting to decrypt key with public key %s", key.PublicKey.String()) } - if err := ks.store.FirstOrCreateEncryptedSecretVRFKey(enckey); err != nil { + if err := ks.orm.FirstOrCreateEncryptedSecretVRFKey(enckey); err != nil { return errors.Wrapf(err, "while saving encrypted key to DB") } ks.keys[key.PublicKey] = *key @@ -134,16 +134,16 @@ func (ks *VRFKeyStore) Import(keyjson []byte, auth string) error { // get retrieves all EncryptedVRFKey's associated with k, or all encrypted // keys if k is nil, or errors. Caller is responsible for locking the store -func (ks *VRFKeyStore) get(k ...vrfkey.PublicKey) ([]*vrfkey.EncryptedVRFKey, +func (ks *VRFKeyStore) get(k ...secp256k1.PublicKey) ([]*EncryptedVRFKey, error) { if len(k) > 1 { return nil, errors.Errorf("can get at most one secret key at a time") } - var where []vrfkey.EncryptedVRFKey + var where []EncryptedVRFKey if len(k) == 1 { // Search for this specific public key - where = append(where, vrfkey.EncryptedVRFKey{PublicKey: k[0]}) + where = append(where, EncryptedVRFKey{PublicKey: k[0]}) } - keys, err := ks.store.FindEncryptedSecretVRFKeys(where...) + keys, err := ks.orm.FindEncryptedSecretVRFKeys(where...) if err != nil { return nil, errors.Wrapf(err, "failed to find public key %s in DB", k) } @@ -152,15 +152,15 @@ func (ks *VRFKeyStore) get(k ...vrfkey.PublicKey) ([]*vrfkey.EncryptedVRFKey, // Get retrieves all EncryptedVRFKey's associated with k, or all encrypted // keys if k is nil, or errors -func (ks *VRFKeyStore) Get(k ...vrfkey.PublicKey) ([]*vrfkey.EncryptedVRFKey, error) { +func (ks *VRFKeyStore) Get(k ...secp256k1.PublicKey) ([]*EncryptedVRFKey, error) { ks.lock.RLock() defer ks.lock.RUnlock() return ks.get(k...) } func (ks *VRFKeyStore) GetSpecificKey( - k vrfkey.PublicKey) (*vrfkey.EncryptedVRFKey, error) { - if k == (vrfkey.PublicKey{}) { + k secp256k1.PublicKey) (*EncryptedVRFKey, error) { + if k == (secp256k1.PublicKey{}) { return nil, fmt.Errorf("can't retrieve zero key") } encryptedKey, err := ks.Get(k) @@ -180,7 +180,7 @@ func (ks *VRFKeyStore) GetSpecificKey( } // ListKeys lists the public keys contained in the db -func (ks *VRFKeyStore) ListKeys() (publicKeys []*vrfkey.PublicKey, err error) { +func (ks *VRFKeyStore) ListKeys() (publicKeys []*secp256k1.PublicKey, err error) { enc, err := ks.Get() if err != nil { return nil, errors.Wrapf(err, "while listing db keys") diff --git a/core/store/vrf_key_store_test.go b/core/services/vrf/vrf_key_store_test.go similarity index 93% rename from core/store/vrf_key_store_test.go rename to core/services/vrf/vrf_key_store_test.go index be956934511..a77125b0546 100644 --- a/core/store/vrf_key_store_test.go +++ b/core/services/vrf/vrf_key_store_test.go @@ -1,10 +1,13 @@ -package store_test +package vrf_test import ( "bytes" "math/big" "testing" + "github.com/smartcontractkit/chainlink/core/services/vrf" + "github.com/smartcontractkit/chainlink/core/utils" + "github.com/ethereum/go-ethereum/eth/ethconfig" "github.com/ethereum/go-ethereum/accounts/abi/bind/backends" @@ -16,10 +19,7 @@ import ( "github.com/stretchr/testify/require" "github.com/smartcontractkit/chainlink/core/internal/cltest" - tvrf "github.com/smartcontractkit/chainlink/core/internal/cltest/vrf" "github.com/smartcontractkit/chainlink/core/internal/gethwrappers/generated/solidity_vrf_verifier_wrapper" - "github.com/smartcontractkit/chainlink/core/services/vrf" - strpkg "github.com/smartcontractkit/chainlink/core/store" ) // NB: For changes to the VRF solidity code to be reflected here, "go generate" @@ -44,7 +44,7 @@ func TestKeyStoreEndToEnd(t *testing.T) { store, cleanup := cltest.NewStore(t) defer cleanup() - ks := strpkg.NewVRFKeyStore(store) + ks := vrf.NewVRFKeyStore(vrf.NewORM(store.DB), utils.GetScryptParams(store.Config)) key, err := ks.CreateKey(phrase) // NB: Varies from run to run. Shouldn't matter, though require.NoError(t, err, "could not create encrypted key") require.NoError(t, ks.Forget(key), "could not forget a created key from in-memory store") @@ -76,7 +76,7 @@ func TestKeyStoreEndToEnd(t *testing.T) { blockHash := common.Hash{} blockNum := 0 preSeed := big.NewInt(10) - s := tvrf.SeedData(t, preSeed, blockHash, blockNum) + s := vrf.TestXXXSeedData(t, preSeed, blockHash, blockNum) proof, err := ks.GenerateProof(key, s) assert.NoError(t, err, "should be able to generate VRF proofs with unlocked keys") @@ -119,7 +119,7 @@ func TestKeyStoreEndToEnd(t *testing.T) { require.NoError(t, err, "failed to import encrypted key to database") err = ks.Import(keyjson, phrase) - require.Equal(t, strpkg.MatchingVRFKeyError, err, "should be prevented from importing a key with a public key already present in the DB") + require.Equal(t, vrf.MatchingVRFKeyError, err, "should be prevented from importing a key with a public key already present in the DB") _, err = ks.GenerateProof(key, s) require.NoError(t, err, "should be able to generate proof with unlocked key") diff --git a/core/services/vrf/vrf_simulate_blockchain_test.go b/core/services/vrf/vrf_simulate_blockchain_test.go index 103df6ac988..b6ae76c289b 100644 --- a/core/services/vrf/vrf_simulate_blockchain_test.go +++ b/core/services/vrf/vrf_simulate_blockchain_test.go @@ -18,7 +18,6 @@ import ( "github.com/smartcontractkit/chainlink/core/services/vrf" "github.com/smartcontractkit/chainlink/core/store/dialects" "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/store/models/vrfkey" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -26,7 +25,7 @@ import ( func registerExistingProvingKey( t *testing.T, coordinator coordinatorUniverse, - provingKey *vrfkey.PrivateKey, + provingKey *vrf.PrivateKey, jobID models.JobID, vrfFee *big.Int, ) { @@ -51,10 +50,10 @@ func TestIntegration_RandomnessRequest(t *testing.T) { app.Start() rawKey := "0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F8179800" - pk, err := vrfkey.NewPublicKeyFromHex(rawKey) + pk, err := secp256k1.NewPublicKeyFromHex(rawKey) require.NoError(t, err) var sk int64 = 1 - provingKey := vrfkey.NewPrivateKeyXXXTestingOnly(big.NewInt(sk)) + provingKey := vrf.NewPrivateKeyXXXTestingOnly(big.NewInt(sk)) require.Equal(t, provingKey.PublicKey, pk, "public key in fixture %s does not match secret key in test %d (which has "+ "public key %s)", pk, sk, provingKey.PublicKey.String()) @@ -139,10 +138,10 @@ func TestIntegration_SharedProvingKey(t *testing.T) { // create job rawKey := "0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F8179800" - pk, err := vrfkey.NewPublicKeyFromHex(rawKey) + pk, err := secp256k1.NewPublicKeyFromHex(rawKey) require.NoError(t, err) var sk int64 = 1 - provingKey := vrfkey.NewPrivateKeyXXXTestingOnly(big.NewInt(sk)) + provingKey := vrf.NewPrivateKeyXXXTestingOnly(big.NewInt(sk)) require.Equal(t, provingKey.PublicKey, pk, "public key in fixture %s does not match secret key in test %d (which has "+ "public key %s)", pk, sk, provingKey.PublicKey.String()) diff --git a/core/store/migrations/0028_vrf_v2.go b/core/store/migrations/0028_vrf_v2.go new file mode 100644 index 00000000000..68bb05421d7 --- /dev/null +++ b/core/store/migrations/0028_vrf_v2.go @@ -0,0 +1,43 @@ +package migrations + +import "gorm.io/gorm" + +const ( + up28 = ` + CREATE TABLE vrf_specs ( + id BIGSERIAL PRIMARY KEY, + public_key text NOT NULL, + coordinator_address bytea NOT NULL, + confirmations bigint NOT NULL, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone NOT NULL + CONSTRAINT coordinator_address_len_chk CHECK (octet_length(coordinator_address) = 20) + ); + ALTER TABLE jobs ADD COLUMN vrf_spec_id INT REFERENCES vrf_specs(id), + DROP CONSTRAINT chk_only_one_spec, + ADD CONSTRAINT chk_only_one_spec CHECK ( + num_nonnulls(offchainreporting_oracle_spec_id, direct_request_spec_id, flux_monitor_spec_id, keeper_spec_id, cron_spec_id, vrf_spec_id) = 1 + ); + ` + down28 = ` + ALTER TABLE jobs DROP CONSTRAINT chk_only_one_spec, + ADD CONSTRAINT chk_only_one_spec CHECK ( + num_nonnulls(offchainreporting_oracle_spec_id, direct_request_spec_id, flux_monitor_spec_id, keeper_spec_id, cron_spec_id) = 1 + ); + + ALTER TABLE jobs DROP COLUMN vrf_spec_id; + DROP TABLE IF EXISTS vrf_specs; + ` +) + +func init() { + Migrations = append(Migrations, &Migration{ + ID: "0028_vrf_v2", + Migrate: func(db *gorm.DB) error { + return db.Exec(up28).Error + }, + Rollback: func(db *gorm.DB) error { + return db.Exec(down28).Error + }, + }) +} diff --git a/core/store/models/vrf_coordinator_interface_test.go b/core/store/models/vrf_coordinator_interface_test.go index a8a971fe4d2..8bbd81c7d23 100644 --- a/core/store/models/vrf_coordinator_interface_test.go +++ b/core/store/models/vrf_coordinator_interface_test.go @@ -4,18 +4,18 @@ import ( "math/big" "testing" - "github.com/smartcontractkit/chainlink/core/assets" - "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/store/models/vrfkey" + "github.com/smartcontractkit/chainlink/core/services/vrf" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" + "github.com/smartcontractkit/chainlink/core/assets" + "github.com/smartcontractkit/chainlink/core/store/models" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) var ( - secretKey = vrfkey.NewPrivateKeyXXXTestingOnly(big.NewInt(1)) + secretKey = vrf.NewPrivateKeyXXXTestingOnly(big.NewInt(1)) keyHash = secretKey.PublicKey.MustHash() jobID = common.BytesToHash([]byte("1234567890abcdef1234567890abcdef")) seed = big.NewInt(1) diff --git a/core/store/models/vrfkey/serialization.go b/core/store/models/vrfkey/serialization.go deleted file mode 100644 index f039e320f5d..00000000000 --- a/core/store/models/vrfkey/serialization.go +++ /dev/null @@ -1,166 +0,0 @@ -package vrfkey - -import ( - "database/sql/driver" - "encoding/json" - "fmt" - "os" - "time" - - "github.com/google/uuid" - - "gorm.io/gorm" - - "github.com/ethereum/go-ethereum/accounts/keystore" - "github.com/pkg/errors" - "github.com/smartcontractkit/chainlink/core/utils" -) - -// EncryptedVRFKey contains encrypted private key to be serialized to DB -// -// We could re-use geth's key handling, here, but this makes it much harder to -// misuse VRF proving keys as ethereum keys or vice versa. -type EncryptedVRFKey struct { - PublicKey PublicKey `gorm:"primary_key"` - VRFKey gethKeyStruct `json:"vrf_key"` - CreatedAt time.Time `json:"-"` - UpdatedAt time.Time `json:"-"` - DeletedAt gorm.DeletedAt `json:"-"` -} - -// passwordPrefix is added to the beginning of the passwords for -// EncryptedVRFKey's, so that VRF keys can't casually be used as ethereum -// keys, and vice-versa. If you want to do that, DON'T. -var passwordPrefix = "don't mix VRF and Ethereum keys!" - -func adulteratedPassword(auth string) string { - return passwordPrefix + auth -} - -// Encrypt returns the key encrypted with passphrase auth -func (k *PrivateKey) Encrypt(auth string, scryptParams utils.ScryptParams) (*EncryptedVRFKey, error) { - keyJSON, err := keystore.EncryptKey(k.gethKey(), adulteratedPassword(auth), - scryptParams.N, scryptParams.P) - if err != nil { - return nil, errors.Wrapf(err, "could not encrypt vrf key") - } - rv := EncryptedVRFKey{} - if e := json.Unmarshal(keyJSON, &rv.VRFKey); e != nil { - return nil, errors.Wrapf(e, "geth returned unexpected key material") - } - rv.PublicKey = k.PublicKey - roundTripKey, err := rv.Decrypt(auth) - if err != nil { - return nil, errors.Wrapf(err, "could not decrypt just-encrypted key!") - } - if !roundTripKey.k.Equal(k.k) || roundTripKey.PublicKey != k.PublicKey { - panic(fmt.Errorf("roundtrip of key resulted in different value")) - } - return &rv, nil -} - -// JSON returns the JSON representation of e, or errors -func (e *EncryptedVRFKey) JSON() ([]byte, error) { - keyJSON, err := json.Marshal(e) - if err != nil { - return nil, errors.Wrapf(err, "could not marshal encrypted key to JSON") - } - return keyJSON, nil -} - -// Decrypt returns the PrivateKey in e, decrypted via auth, or an error -func (e *EncryptedVRFKey) Decrypt(auth string) (*PrivateKey, error) { - // NOTE: We do this shuffle to an anonymous struct - // solely to add a a throwaway UUID, so we can leverage - // the keystore.DecryptKey from the geth which requires it - // as of 1.10.0. - keyJSON, err := json.Marshal(struct { - Address string `json:"address"` - Crypto keystore.CryptoJSON `json:"crypto"` - Version int `json:"version"` - Id string `json:"id"` - }{ - Address: e.VRFKey.Address, - Crypto: e.VRFKey.Crypto, - Version: e.VRFKey.Version, - Id: uuid.New().String(), - }) - if err != nil { - return nil, errors.Wrapf(err, "while marshaling key for decryption") - } - gethKey, err := keystore.DecryptKey(keyJSON, adulteratedPassword(auth)) - if err != nil { - return nil, errors.Wrapf(err, "could not decrypt key %s", - e.PublicKey.String()) - } - return fromGethKey(gethKey), nil -} - -// WriteToDisk writes the JSON representation of e to given file path, and -// ensures the file has appropriate access permissions -func (e *EncryptedVRFKey) WriteToDisk(path string) error { - keyJSON, err := e.JSON() - if err != nil { - return errors.Wrapf(err, "while marshaling key to save to %s", path) - } - userReadWriteOtherNoAccess := os.FileMode(0600) - return utils.WriteFileWithMaxPerms(path, keyJSON, userReadWriteOtherNoAccess) -} - -// MarshalText renders k as a text string -func (k PublicKey) MarshalText() ([]byte, error) { - return []byte(k.String()), nil -} - -// UnmarshalText reads a PublicKey into k from text, or errors -func (k *PublicKey) UnmarshalText(text []byte) error { - if err := k.SetFromHex(string(text)); err != nil { - return errors.Wrapf(err, "while parsing %s as public key", text) - } - return nil -} - -// Value marshals PublicKey to be saved in the DB -func (k PublicKey) Value() (driver.Value, error) { - return k.String(), nil -} - -// Scan reconstructs a PublicKey from a DB record of it. -func (k *PublicKey) Scan(value interface{}) error { - rawKey, ok := value.(string) - if !ok { - return errors.Wrap(fmt.Errorf("unable to convert %+v of type %T to PublicKey", value, value), "scan failure") - } - if err := k.SetFromHex(rawKey); err != nil { - return errors.Wrapf(err, "while scanning %s as PublicKey", rawKey) - } - return nil -} - -// Copied from go-ethereum/accounts/keystore/key.go's encryptedKeyJSONV3 -type gethKeyStruct struct { - Address string `json:"address"` - Crypto keystore.CryptoJSON `json:"crypto"` - Version int `json:"version"` -} - -func (k gethKeyStruct) Value() (driver.Value, error) { - return json.Marshal(&k) -} - -func (k *gethKeyStruct) Scan(value interface{}) error { - // With sqlite gorm driver, we get a []byte, here. With postgres, a string! - // https://github.com/jinzhu/gorm/issues/2276 - var toUnmarshal []byte - switch s := value.(type) { - case []byte: - toUnmarshal = s - case string: - toUnmarshal = []byte(s) - default: - return errors.Wrap( - fmt.Errorf("unable to convert %+v of type %T to gethKeyStruct", - value, value), "scan failure") - } - return json.Unmarshal(toUnmarshal, k) -} diff --git a/core/store/orm/orm.go b/core/store/orm/orm.go index 6f84f711016..81924fae22f 100644 --- a/core/store/orm/orm.go +++ b/core/store/orm/orm.go @@ -35,7 +35,6 @@ import ( "github.com/smartcontractkit/chainlink/core/services/postgres" "github.com/smartcontractkit/chainlink/core/services/synchronization" "github.com/smartcontractkit/chainlink/core/store/models" - "github.com/smartcontractkit/chainlink/core/store/models/vrfkey" "github.com/smartcontractkit/chainlink/core/utils" "go.uber.org/multierr" gormpostgres "gorm.io/driver/postgres" @@ -1450,35 +1449,6 @@ func (orm *ORM) CreateKeyIfNotExists(k models.Key) error { return err } -// FirstOrCreateEncryptedVRFKey returns the first key found or creates a new one in the orm. -func (orm *ORM) FirstOrCreateEncryptedSecretVRFKey(k *vrfkey.EncryptedVRFKey) error { - return orm.DB.FirstOrCreate(k).Error -} - -// ArchiveEncryptedVRFKey soft-deletes k from the encrypted keys table, or errors -func (orm *ORM) ArchiveEncryptedSecretVRFKey(k *vrfkey.EncryptedVRFKey) error { - return orm.DB.Delete(k).Error -} - -// DeleteEncryptedVRFKey deletes k from the encrypted keys table, or errors -func (orm *ORM) DeleteEncryptedSecretVRFKey(k *vrfkey.EncryptedVRFKey) error { - return orm.DB.Unscoped().Delete(k).Error -} - -// FindEncryptedVRFKeys retrieves matches to where from the encrypted keys table, or errors -func (orm *ORM) FindEncryptedSecretVRFKeys(where ...vrfkey.EncryptedVRFKey) ( - retrieved []*vrfkey.EncryptedVRFKey, err error) { - if err := orm.MustEnsureAdvisoryLock(); err != nil { - return nil, err - } - var anonWhere []interface{} // Find needs "where" contents coerced to interface{} - for _, constraint := range where { - c := constraint - anonWhere = append(anonWhere, &c) - } - return retrieved, orm.DB.Find(&retrieved, anonWhere...).Error -} - // GetRoundRobinAddress queries the database for the address of a random ethereum key derived from the id. // This takes an optional param for a slice of addresses it should pick from. Leave empty to pick from all // addresses in the database. diff --git a/core/store/store.go b/core/store/store.go index b924af6fce1..f7ede4f9581 100644 --- a/core/store/store.go +++ b/core/store/store.go @@ -8,6 +8,8 @@ import ( "path/filepath" "sync" + "github.com/smartcontractkit/chainlink/core/services/vrf" + "github.com/coreos/go-semver/semver" "github.com/smartcontractkit/chainlink/core/logger" "github.com/smartcontractkit/chainlink/core/services/periodicbackup" @@ -46,7 +48,7 @@ type Store struct { Config *orm.Config Clock utils.AfterNower KeyStore KeyStoreInterface - VRFKeyStore *VRFKeyStore + VRFKeyStore *vrf.VRFKeyStore OCRKeyStore *offchainreporting.KeyStore EthClient eth.Client NotifyNewEthTx NotifyNewEthTx @@ -110,7 +112,7 @@ func newStoreWithKeyStore( EthClient: ethClient, closeOnce: &sync.Once{}, } - store.VRFKeyStore = NewVRFKeyStore(store) + store.VRFKeyStore = vrf.NewVRFKeyStore(vrf.NewORM(orm.DB), scryptParams) return store, nil } diff --git a/core/testdata/testspecs/v2_specs.go b/core/testdata/testspecs/v2_specs.go new file mode 100644 index 00000000000..7e935145d16 --- /dev/null +++ b/core/testdata/testspecs/v2_specs.go @@ -0,0 +1,119 @@ +package testspecs + +import "strings" + +var ( + OCRSpec = ` +type = "offchainreporting" +schemaVersion = 1 +name = "web oracle spec" +contractAddress = "0x613a38AC1659769640aaE063C651F48E0250454C" +p2pPeerID = "12D3KooWApUJaQB2saFjyEUfq6BmysnsSnhLnY5CF9tURYVKgoXK" +p2pBootstrapPeers = [ + "/dns4/chain.link/tcp/1234/p2p/16Uiu2HAm58SP7UL8zsnpeuwHfytLocaqgnyaYKP8wu7qRdrixLju", +] +isBootstrapPeer = false +keyBundleID = "7f993fb701b3410b1f6e8d4d93a7462754d24609b9b31a4fe64a0cb475a4d934" +monitoringEndpoint = "chain.link:4321" +transmitterAddress = "0xF67D0290337bca0847005C7ffD1BC75BA9AAE6e4" +observationTimeout = "10s" +blockchainTimeout = "20s" +contractConfigTrackerSubscribeInterval = "2m" +contractConfigTrackerPollInterval = "1m" +contractConfigConfirmations = 3 +observationSource = """ + // data source 1 + ds1 [type=bridge name=voter_turnout]; + ds1_parse [type=jsonparse path="one,two"]; + ds1_multiply [type=multiply times=1.23]; + + // data source 2 + ds2 [type=http method=GET url="https://chain.link/voter_turnout/USA-2020" requestData="{\\"hi\\": \\"hello\\"}"]; + ds2_parse [type=jsonparse path="three,four"]; + ds2_multiply [type=multiply times=4.56]; + + ds1 -> ds1_parse -> ds1_multiply -> answer1; + ds2 -> ds2_parse -> ds2_multiply -> answer1; + + answer1 [type=median index=0]; + answer2 [type=bridge name=election_winner index=1]; +""" +` + KeeperSpec = ` +type = "keeper" +schemaVersion = 1 +name = "example keeper spec" +contractAddress = "0x9E40733cC9df84636505f4e6Db28DCa0dC5D1bba" +fromAddress = "0xa8037A20989AFcBC51798de9762b351D63ff462e" +` + CronSpec = ` +type = "cron" +schemaVersion = 1 +schedule = "0 0 1 1 *" +observationSource = """ +ds [type=http method=GET url="https://chain.link/ETH-USD"]; +ds_parse [type=jsonparse path="data,price"]; +ds_multiply [type=multiply times=100]; +ds -> ds_parse -> ds_multiply; +""" +` + DirectRequestSpec = ` +type = "directrequest" +schemaVersion = 1 +name = "example eth request event spec" +contractAddress = "0x613a38AC1659769640aaE063C651F48E0250454C" +jobID = "0EEC7E1D-D0D2-476C-A1A8-72DFB6633F46" +observationSource = """ + ds1 [type=http method=GET url="http://example.com" allowunrestrictednetworkaccess="true"]; + ds1_parse [type=jsonparse path="USD"]; + ds1_multiply [type=multiply times=100]; + ds1 -> ds1_parse -> ds1_multiply; +""" +` + FluxMonitorSpec = ` +type = "fluxmonitor" +schemaVersion = 1 +name = "example flux monitor spec" +contractAddress = "0x3cCad4715152693fE3BC4460591e3D3Fbd071b42" +precision = 2 +threshold = 0.5 +absoluteThreshold = 0.0 # optional + +idleTimerPeriod = "1s" +idleTimerDisabled = false + +pollTimerPeriod = "1m" +pollTimerDisabled = false + +observationSource = """ +// data source 1 +ds1 [type=http method=GET url="https://pricesource1.com" requestData="{\\"coin\\": \\"ETH\\", \\"market\\": \\"USD\\"}"]; +ds1_parse [type=jsonparse path="latest"]; + +// data source 2 +ds2 [type=http method=GET url="https://pricesource1.com" requestData="{\\"coin\\": \\"ETH\\", \\"market\\": \\"USD\\"}"]; +ds2_parse [type=jsonparse path="latest"]; + +ds1 -> ds1_parse -> answer1; +ds2 -> ds2_parse -> answer1; + +answer1 [type=median index=0]; +""" +` + VRFSpec = ` +jobID = "123e4567-e89b-12d3-a456-426655440000" +type = "vrf" +schemaVersion = 1 +name = "vrf-primary" +coordinatorAddress = "0xABA5eDc1a551E55b1A570c0e1f1055e5BE11eca7" +confirmations = 6 +publicKey = "0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F8179800" +observationSource=""" +getrandomvalue [type=vrf]; +""" +` +) + +func OCRSpecWithTransmitterAddress(ta string) string { + return strings.Replace(OCRSpec, "0xF67D0290337bca0847005C7ffD1BC75BA9AAE6e4", ta, 1) +} diff --git a/core/web/jobs_controller.go b/core/web/jobs_controller.go index c54041468e3..6aa9797df33 100644 --- a/core/web/jobs_controller.go +++ b/core/web/jobs_controller.go @@ -3,6 +3,8 @@ package web import ( "net/http" + "github.com/smartcontractkit/chainlink/core/services/vrf" + "github.com/smartcontractkit/chainlink/core/services/directrequest" "github.com/smartcontractkit/chainlink/core/services/keeper" "github.com/smartcontractkit/chainlink/core/services/offchainreporting" @@ -107,6 +109,8 @@ func (jc *JobsController) Create(c *gin.Context) { js, err = keeper.ValidatedKeeperSpec(request.TOML) case job.Cron: js, err = cron.ValidateCronSpec(request.TOML) + case job.VRF: + js, err = vrf.ValidateVRFSpec(request.TOML) default: jsonAPIError(c, http.StatusUnprocessableEntity, errors.Errorf("unknown job type: %s", genericJS.Type)) } diff --git a/core/web/jobs_controller_test.go b/core/web/jobs_controller_test.go index dcccd1a7054..102e0265dd2 100644 --- a/core/web/jobs_controller_test.go +++ b/core/web/jobs_controller_test.go @@ -7,10 +7,11 @@ import ( "fmt" "io/ioutil" "net/http" - "strings" "testing" "time" + "github.com/smartcontractkit/chainlink/core/testdata/testspecs" + "github.com/pelletier/go-toml" "github.com/smartcontractkit/chainlink/core/internal/cltest" "github.com/smartcontractkit/chainlink/core/services/directrequest" @@ -19,7 +20,6 @@ import ( "github.com/smartcontractkit/chainlink/core/web" "github.com/smartcontractkit/chainlink/core/web/presenters" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" "gopkg.in/guregu/null.v4" ) @@ -84,175 +84,149 @@ func TestJobsController_Create_ValidationFailure_OffchainReportingSpec(t *testin } } -func TestJobsController_Create_HappyPath_OffchainReportingSpec(t *testing.T) { +func TestJobController_Create_HappyPath(t *testing.T) { app, client := setupJobsControllerTests(t) - - toml := string(cltest.MustReadFile(t, "../testdata/tomlspecs/oracle-spec.toml")) - toml = strings.Replace(toml, "0xF67D0290337bca0847005C7ffD1BC75BA9AAE6e4", app.Key.Address.Hex(), 1) - body, _ := json.Marshal(web.CreateJobRequest{ - TOML: toml, - }) - response, cleanup := client.Post("/v2/jobs", bytes.NewReader(body)) - t.Cleanup(cleanup) - require.Equal(t, http.StatusOK, response.StatusCode) - - jb := job.Job{} - require.NoError(t, app.Store.DB.Preload("OffchainreportingOracleSpec").First(&jb).Error) - - resource := presenters.JobResource{} - err := web.ParseJSONAPIResponse(cltest.ParseResponseBody(t, response), &resource) - assert.NoError(t, err) - - assert.Equal(t, "web oracle spec", jb.Name.ValueOrZero()) - assert.Equal(t, jb.OffchainreportingOracleSpec.P2PPeerID, resource.OffChainReportingSpec.P2PPeerID) - assert.Equal(t, jb.OffchainreportingOracleSpec.P2PBootstrapPeers, resource.OffChainReportingSpec.P2PBootstrapPeers) - assert.Equal(t, jb.OffchainreportingOracleSpec.IsBootstrapPeer, resource.OffChainReportingSpec.IsBootstrapPeer) - assert.Equal(t, jb.OffchainreportingOracleSpec.EncryptedOCRKeyBundleID, resource.OffChainReportingSpec.EncryptedOCRKeyBundleID) - assert.Equal(t, jb.OffchainreportingOracleSpec.TransmitterAddress, resource.OffChainReportingSpec.TransmitterAddress) - assert.Equal(t, jb.OffchainreportingOracleSpec.ObservationTimeout, resource.OffChainReportingSpec.ObservationTimeout) - assert.Equal(t, jb.OffchainreportingOracleSpec.BlockchainTimeout, resource.OffChainReportingSpec.BlockchainTimeout) - assert.Equal(t, jb.OffchainreportingOracleSpec.ContractConfigTrackerSubscribeInterval, resource.OffChainReportingSpec.ContractConfigTrackerSubscribeInterval) - assert.Equal(t, jb.OffchainreportingOracleSpec.ContractConfigTrackerSubscribeInterval, resource.OffChainReportingSpec.ContractConfigTrackerSubscribeInterval) - assert.Equal(t, jb.OffchainreportingOracleSpec.ContractConfigConfirmations, resource.OffChainReportingSpec.ContractConfigConfirmations) - assert.NotNil(t, resource.PipelineSpec.DotDAGSource) - - // Sanity check to make sure it inserted correctly - require.Equal(t, models.EIP55Address("0x613a38AC1659769640aaE063C651F48E0250454C"), jb.OffchainreportingOracleSpec.ContractAddress) -} - -func TestJobsController_Create_HappyPath_KeeperSpec(t *testing.T) { - app, cleanup := cltest.NewApplicationWithKey(t) - t.Cleanup(cleanup) - require.NoError(t, app.Start()) - - client := app.NewHTTPClient() - - tomlBytes := cltest.MustReadFile(t, "../testdata/tomlspecs/keeper-spec.toml") - body, _ := json.Marshal(web.CreateJobRequest{ - TOML: string(tomlBytes), - }) - response, cleanup := client.Post("/v2/jobs", bytes.NewReader(body)) - t.Cleanup(cleanup) - require.Equal(t, http.StatusOK, response.StatusCode) - - jb := job.Job{} - require.NoError(t, app.Store.DB.Preload("KeeperSpec").First(&jb).Error) - - resource := presenters.JobResource{} - err := web.ParseJSONAPIResponse(cltest.ParseResponseBody(t, response), &resource) - assert.NoError(t, err) - - require.Equal(t, resource.KeeperSpec.ContractAddress, jb.KeeperSpec.ContractAddress) - require.Equal(t, resource.KeeperSpec.FromAddress, jb.KeeperSpec.FromAddress) - assert.Equal(t, "example keeper spec", jb.Name.ValueOrZero()) - - // Sanity check to make sure it inserted correctly - require.Equal(t, models.EIP55Address("0x9E40733cC9df84636505f4e6Db28DCa0dC5D1bba"), jb.KeeperSpec.ContractAddress) - require.Equal(t, models.EIP55Address("0xa8037A20989AFcBC51798de9762b351D63ff462e"), jb.KeeperSpec.FromAddress) -} - -func TestJobsController_Create_CronRequestSpec(t *testing.T) { - ethClient, _, assertMocksCalled := cltest.NewEthMocksWithStartupAssertions(t) - t.Cleanup(assertMocksCalled) - app, cleanup := cltest.NewApplicationWithKey(t, - ethClient, - ) - t.Cleanup(cleanup) - require.NoError(t, app.StartAndConnect()) - - client := app.NewHTTPClient() - - tomlBytes := cltest.MustReadFile(t, "../testdata/tomlspecs/cron-spec.toml") - body, _ := json.Marshal(web.CreateJobRequest{ - TOML: string(tomlBytes), - }) - response, cleanup := client.Post("/v2/jobs", bytes.NewReader(body)) - defer cleanup() - require.Equal(t, http.StatusOK, response.StatusCode) - - jb := job.Job{} - require.NoError(t, app.Store.DB.Preload("CronSpec").First(&jb).Error) - - resource := presenters.JobResource{} - err := web.ParseJSONAPIResponse(cltest.ParseResponseBody(t, response), &resource) - assert.NoError(t, err) - assert.NotNil(t, resource.PipelineSpec.DotDAGSource) - require.Equal(t, "0 0 1 1 *", jb.CronSpec.CronSchedule) -} - -func TestJobsController_Create_HappyPath_DirectRequestSpec(t *testing.T) { - ethClient, _, assertMocksCalled := cltest.NewEthMocksWithStartupAssertions(t) - t.Cleanup(assertMocksCalled) - app, cleanup := cltest.NewApplicationWithKey(t, - ethClient, - ) - t.Cleanup(cleanup) - require.NoError(t, app.Start()) - ethClient.On("SubscribeFilterLogs", mock.Anything, mock.Anything, mock.Anything).Maybe().Return(cltest.EmptyMockSubscription(), nil) - - client := app.NewHTTPClient() - - tomlBytes := cltest.MustReadFile(t, "../testdata/tomlspecs/direct-request-spec.toml") - body, _ := json.Marshal(web.CreateJobRequest{ - TOML: string(tomlBytes), - }) - response, cleanup := client.Post("/v2/jobs", bytes.NewReader(body)) - t.Cleanup(cleanup) - require.Equal(t, http.StatusOK, response.StatusCode) - - jb := job.Job{} - require.NoError(t, app.Store.DB.Preload("DirectRequestSpec").First(&jb).Error) - - resource := presenters.JobResource{} - err := web.ParseJSONAPIResponse(cltest.ParseResponseBody(t, response), &resource) - assert.NoError(t, err) - - assert.Equal(t, "example eth request event spec", jb.Name.ValueOrZero()) - assert.NotNil(t, resource.PipelineSpec.DotDAGSource) - - // Sanity check to make sure it inserted correctly - require.Equal(t, models.EIP55Address("0x613a38AC1659769640aaE063C651F48E0250454C"), jb.DirectRequestSpec.ContractAddress) - - require.NotZero(t, jb.DirectRequestSpec.OnChainJobSpecID[:]) -} - -func TestJobsController_Create_HappyPath_FluxMonitorSpec(t *testing.T) { - ethClient, _, assertMocksCalled := cltest.NewEthMocksWithStartupAssertions(t) - t.Cleanup(assertMocksCalled) - app, cleanup := cltest.NewApplicationWithKey(t, - ethClient, - ) - t.Cleanup(cleanup) - require.NoError(t, app.Start()) - ethClient.On("SubscribeFilterLogs", mock.Anything, mock.Anything, mock.Anything).Maybe().Return(cltest.EmptyMockSubscription(), nil) - - client := app.NewHTTPClient() - - tomlBytes := cltest.MustReadFile(t, "../testdata/tomlspecs/flux-monitor-spec.toml") - body, _ := json.Marshal(web.CreateJobRequest{ - TOML: string(tomlBytes), - }) - - response, cleanup := client.Post("/v2/jobs", bytes.NewReader(body)) - t.Cleanup(cleanup) - require.Equal(t, http.StatusOK, response.StatusCode) - - jb := job.Job{} - require.NoError(t, app.Store.DB.Preload("FluxMonitorSpec").First(&jb).Error) - - resource := presenters.JobResource{} - err := web.ParseJSONAPIResponse(cltest.ParseResponseBody(t, response), &resource) - assert.NoError(t, err) - t.Log() - - assert.Equal(t, "example flux monitor spec", jb.Name.ValueOrZero()) - assert.NotNil(t, resource.PipelineSpec.DotDAGSource) - assert.Equal(t, models.EIP55Address("0x3cCad4715152693fE3BC4460591e3D3Fbd071b42"), jb.FluxMonitorSpec.ContractAddress) - assert.Equal(t, time.Second, jb.FluxMonitorSpec.IdleTimerPeriod) - assert.Equal(t, false, jb.FluxMonitorSpec.IdleTimerDisabled) - assert.Equal(t, int32(2), jb.FluxMonitorSpec.Precision) - assert.Equal(t, float32(0.5), jb.FluxMonitorSpec.Threshold) - assert.Equal(t, float32(0), jb.FluxMonitorSpec.AbsoluteThreshold) + var tt = []struct { + name string + toml string + assertion func(t *testing.T, r *http.Response) + }{ + { + name: "offchain reporting", + toml: testspecs.OCRSpecWithTransmitterAddress(app.Key.Address.Hex()), + assertion: func(t *testing.T, r *http.Response) { + require.Equal(t, http.StatusOK, r.StatusCode) + + jb := job.Job{} + require.NoError(t, app.Store.DB.Preload("OffchainreportingOracleSpec").First(&jb, "type = ?", job.OffchainReporting).Error) + + resource := presenters.JobResource{} + err := web.ParseJSONAPIResponse(cltest.ParseResponseBody(t, r), &resource) + assert.NoError(t, err) + + assert.Equal(t, "web oracle spec", jb.Name.ValueOrZero()) + assert.Equal(t, jb.OffchainreportingOracleSpec.P2PPeerID, resource.OffChainReportingSpec.P2PPeerID) + assert.Equal(t, jb.OffchainreportingOracleSpec.P2PBootstrapPeers, resource.OffChainReportingSpec.P2PBootstrapPeers) + assert.Equal(t, jb.OffchainreportingOracleSpec.IsBootstrapPeer, resource.OffChainReportingSpec.IsBootstrapPeer) + assert.Equal(t, jb.OffchainreportingOracleSpec.EncryptedOCRKeyBundleID, resource.OffChainReportingSpec.EncryptedOCRKeyBundleID) + assert.Equal(t, jb.OffchainreportingOracleSpec.TransmitterAddress, resource.OffChainReportingSpec.TransmitterAddress) + assert.Equal(t, jb.OffchainreportingOracleSpec.ObservationTimeout, resource.OffChainReportingSpec.ObservationTimeout) + assert.Equal(t, jb.OffchainreportingOracleSpec.BlockchainTimeout, resource.OffChainReportingSpec.BlockchainTimeout) + assert.Equal(t, jb.OffchainreportingOracleSpec.ContractConfigTrackerSubscribeInterval, resource.OffChainReportingSpec.ContractConfigTrackerSubscribeInterval) + assert.Equal(t, jb.OffchainreportingOracleSpec.ContractConfigTrackerSubscribeInterval, resource.OffChainReportingSpec.ContractConfigTrackerSubscribeInterval) + assert.Equal(t, jb.OffchainreportingOracleSpec.ContractConfigConfirmations, resource.OffChainReportingSpec.ContractConfigConfirmations) + assert.NotNil(t, resource.PipelineSpec.DotDAGSource) + // Sanity check to make sure it inserted correctly + require.Equal(t, models.EIP55Address("0x613a38AC1659769640aaE063C651F48E0250454C"), jb.OffchainreportingOracleSpec.ContractAddress) + }, + }, + { + name: "keeper", + toml: testspecs.KeeperSpec, + assertion: func(t *testing.T, r *http.Response) { + require.Equal(t, http.StatusOK, r.StatusCode) + + jb := job.Job{} + require.NoError(t, app.Store.DB.Preload("KeeperSpec").First(&jb, "type = ?", job.Keeper).Error) + + resource := presenters.JobResource{} + b := cltest.ParseResponseBody(t, r) + err := web.ParseJSONAPIResponse(b, &resource) + require.NoError(t, err) + require.NotNil(t, resource.KeeperSpec) + require.NotNil(t, jb.KeeperSpec) + + require.Equal(t, resource.KeeperSpec.ContractAddress, jb.KeeperSpec.ContractAddress) + require.Equal(t, resource.KeeperSpec.FromAddress, jb.KeeperSpec.FromAddress) + assert.Equal(t, "example keeper spec", jb.Name.ValueOrZero()) + + // Sanity check to make sure it inserted correctly + require.Equal(t, models.EIP55Address("0x9E40733cC9df84636505f4e6Db28DCa0dC5D1bba"), jb.KeeperSpec.ContractAddress) + require.Equal(t, models.EIP55Address("0xa8037A20989AFcBC51798de9762b351D63ff462e"), jb.KeeperSpec.FromAddress) + }, + }, + { + name: "cron", + toml: testspecs.CronSpec, + assertion: func(t *testing.T, r *http.Response) { + require.Equal(t, http.StatusOK, r.StatusCode) + jb := job.Job{} + require.NoError(t, app.Store.DB.Preload("CronSpec").First(&jb, "type = ?", job.Cron).Error) + resource := presenters.JobResource{} + err := web.ParseJSONAPIResponse(cltest.ParseResponseBody(t, r), &resource) + assert.NoError(t, err) + assert.NotNil(t, resource.PipelineSpec.DotDAGSource) + require.Equal(t, "0 0 1 1 *", jb.CronSpec.CronSchedule) + }, + }, + { + name: "directrequest", + toml: testspecs.DirectRequestSpec, + assertion: func(t *testing.T, r *http.Response) { + require.Equal(t, http.StatusOK, r.StatusCode) + jb := job.Job{} + require.NoError(t, app.Store.DB.Preload("DirectRequestSpec").First(&jb, "type = ?", job.DirectRequest).Error) + resource := presenters.JobResource{} + err := web.ParseJSONAPIResponse(cltest.ParseResponseBody(t, r), &resource) + assert.NoError(t, err) + assert.Equal(t, "example eth request event spec", jb.Name.ValueOrZero()) + assert.NotNil(t, resource.PipelineSpec.DotDAGSource) + // Sanity check to make sure it inserted correctly + require.Equal(t, models.EIP55Address("0x613a38AC1659769640aaE063C651F48E0250454C"), jb.DirectRequestSpec.ContractAddress) + require.NotZero(t, jb.DirectRequestSpec.OnChainJobSpecID[:]) + }, + }, + { + name: "fluxmonitor", + toml: testspecs.FluxMonitorSpec, + assertion: func(t *testing.T, r *http.Response) { + require.Equal(t, http.StatusOK, r.StatusCode) + jb := job.Job{} + require.NoError(t, app.Store.DB.Preload("FluxMonitorSpec").First(&jb, "type = ?", job.FluxMonitor).Error) + resource := presenters.JobResource{} + err := web.ParseJSONAPIResponse(cltest.ParseResponseBody(t, r), &resource) + assert.NoError(t, err) + assert.Equal(t, "example flux monitor spec", jb.Name.ValueOrZero()) + assert.NotNil(t, resource.PipelineSpec.DotDAGSource) + assert.Equal(t, models.EIP55Address("0x3cCad4715152693fE3BC4460591e3D3Fbd071b42"), jb.FluxMonitorSpec.ContractAddress) + assert.Equal(t, time.Second, jb.FluxMonitorSpec.IdleTimerPeriod) + assert.Equal(t, false, jb.FluxMonitorSpec.IdleTimerDisabled) + assert.Equal(t, int32(2), jb.FluxMonitorSpec.Precision) + assert.Equal(t, float32(0.5), jb.FluxMonitorSpec.Threshold) + assert.Equal(t, float32(0), jb.FluxMonitorSpec.AbsoluteThreshold) + }, + }, + { + name: "vrf", + toml: testspecs.VRFSpec, + assertion: func(t *testing.T, r *http.Response) { + require.Equal(t, http.StatusOK, r.StatusCode) + jb := job.Job{} + require.NoError(t, app.Store.DB.Preload("VRFSpec").First(&jb, "type = ?", job.VRF).Error) + resp := cltest.ParseResponseBody(t, r) + resource := presenters.JobResource{} + err := web.ParseJSONAPIResponse(resp, &resource) + require.NoError(t, err) + assert.NotNil(t, resource.PipelineSpec.DotDAGSource) + assert.Equal(t, uint32(6), resource.VRFSpec.Confirmations) + assert.Equal(t, jb.VRFSpec.Confirmations, resource.VRFSpec.Confirmations) + assert.Equal(t, "0xABA5eDc1a551E55b1A570c0e1f1055e5BE11eca7", resource.VRFSpec.CoordinatorAddress.Hex()) + assert.Equal(t, jb.VRFSpec.CoordinatorAddress.Hex(), resource.VRFSpec.CoordinatorAddress.Hex()) + }, + }, + } + for _, tc := range tt { + c := tc + t.Run(c.name, func(t *testing.T) { + body, err := json.Marshal(web.CreateJobRequest{ + TOML: c.toml, + }) + require.NoError(t, err) + response, cleanup := client.Post("/v2/jobs", bytes.NewReader(body)) + defer cleanup() + c.assertion(t, response) + }) + } } func TestJobsController_Index_HappyPath(t *testing.T) { @@ -346,8 +320,6 @@ func runDirectRequestJobSpecAssertions(t *testing.T, ereJobSpecFromFile job.Job, } func setupJobsControllerTests(t *testing.T) (*cltest.TestApplication, cltest.HTTPClientCleaner) { - t.Parallel() - app, cleanup := cltest.NewApplicationWithKey(t) t.Cleanup(cleanup) require.NoError(t, app.Start()) diff --git a/core/web/presenters/job.go b/core/web/presenters/job.go index 4d78d4e7391..506c2f5008b 100644 --- a/core/web/presenters/job.go +++ b/core/web/presenters/job.go @@ -3,6 +3,8 @@ package presenters import ( "time" + "github.com/smartcontractkit/chainlink/core/services/signatures/secp256k1" + "github.com/lib/pq" "github.com/smartcontractkit/chainlink/core/assets" "github.com/smartcontractkit/chainlink/core/services/job" @@ -18,14 +20,12 @@ func (t JobSpecType) String() string { } const ( - // DirectRequestJobSpec defines a Direct Request Job - DirectRequestJobSpec JobSpecType = "directrequest" - // FluxMonitorJobSpec defines a Flux Monitor Job - FluxMonitorJobSpec JobSpecType = "fluxmonitor" - // OffChainReportingJobSpec defines an OCR Job + DirectRequestJobSpec JobSpecType = "directrequest" + FluxMonitorJobSpec JobSpecType = "fluxmonitor" OffChainReportingJobSpec JobSpecType = "offchainreporting" - // Keeper defines a Keeper Job - KeeperJobSpec JobSpecType = "keeper" + KeeperJobSpec JobSpecType = "keeper" + CronJobSpec JobSpecType = "cron" + VRFJobSpec JobSpecType = "vrf" ) // DirectRequestSpec defines the spec details of a DirectRequest Job @@ -169,6 +169,24 @@ func NewCronSpec(spec *job.CronSpec) *CronSpec { } } +type VRFSpec struct { + CoordinatorAddress models.EIP55Address `toml:"coordinatorAddress"` + PublicKey secp256k1.PublicKey `toml:"publicKey"` + Confirmations uint32 `toml:"confirmations"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` +} + +func NewVRFSpec(spec *job.VRFSpec) *VRFSpec { + return &VRFSpec{ + CoordinatorAddress: spec.CoordinatorAddress, + PublicKey: spec.PublicKey, + Confirmations: spec.Confirmations, + CreatedAt: spec.CreatedAt, + UpdatedAt: spec.UpdatedAt, + } +} + // JobError represents errors on the job type JobError struct { ID int64 `json:"id"` @@ -200,6 +218,7 @@ type JobResource struct { OffChainReportingSpec *OffChainReportingSpec `json:"offChainReportingOracleSpec"` KeeperSpec *KeeperSpec `json:"keeperSpec"` CronSpec *CronSpec `json:"cronSpec"` + VRFSpec *VRFSpec `json:"vrfSpec"` PipelineSpec PipelineSpec `json:"pipelineSpec"` Errors []JobError `json:"errors"` } @@ -226,6 +245,8 @@ func NewJobResource(j job.Job) *JobResource { resource.CronSpec = NewCronSpec(j.CronSpec) case job.Keeper: resource.KeeperSpec = NewKeeperSpec(j.KeeperSpec) + case job.VRF: + resource.VRFSpec = NewVRFSpec(j.VRFSpec) } jes := []JobError{} diff --git a/core/web/presenters/job_test.go b/core/web/presenters/job_test.go index dff14524c2a..c71aae7e803 100644 --- a/core/web/presenters/job_test.go +++ b/core/web/presenters/job_test.go @@ -91,6 +91,7 @@ func TestJob(t *testing.T) { "fluxMonitorSpec": null, "keeperSpec": null, "cronSpec": null, + "vrfSpec": null, "errors": [] } } @@ -152,6 +153,7 @@ func TestJob(t *testing.T) { "directRequestSpec": null, "keeperSpec": null, "cronSpec": null, + "vrfSpec": null, "errors": [] } } @@ -218,6 +220,7 @@ func TestJob(t *testing.T) { "directRequestSpec": null, "keeperSpec": null, "cronSpec": null, + "vrfSpec": null, "errors": [] } } @@ -266,6 +269,7 @@ func TestJob(t *testing.T) { "directRequestSpec": null, "offChainReportingOracleSpec": null, "cronSpec": null, + "vrfSpec": null, "errors": [] } } @@ -312,6 +316,7 @@ func TestJob(t *testing.T) { "directRequestSpec": null, "keeperSpec": null, "offChainReportingOracleSpec": null, + "vrfSpec": null, "errors": [] } } @@ -370,6 +375,7 @@ func TestJob(t *testing.T) { "directRequestSpec": null, "cronSpec": null, "offChainReportingOracleSpec": null, + "vrfSpec": null, "errors": [{ "id": 200, "description": "some error",