Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

script for manually fulfilling old VRF V2 requests #11731

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 4 additions & 41 deletions core/scripts/common/vrf/setup-envs/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"encoding/json"
"flag"
"fmt"
"io"
"math/big"
"os"
"strings"
Expand All @@ -17,6 +16,7 @@ import (
helpers "github.com/smartcontractkit/chainlink/core/scripts/common"
"github.com/smartcontractkit/chainlink/core/scripts/common/vrf/constants"
"github.com/smartcontractkit/chainlink/core/scripts/common/vrf/model"
"github.com/smartcontractkit/chainlink/core/scripts/common/vrf/util"
"github.com/smartcontractkit/chainlink/core/scripts/vrfv2/testnet/v2scripts"
"github.com/smartcontractkit/chainlink/core/scripts/vrfv2plus/testnet/v2plusscripts"
clcmd "github.com/smartcontractkit/chainlink/v2/core/cmd"
Expand All @@ -25,28 +25,6 @@ import (
"github.com/smartcontractkit/chainlink/v2/core/web/presenters"
)

func newApp(remoteNodeURL string, writer io.Writer) (*clcmd.Shell, *cli.App) {
prompter := clcmd.NewTerminalPrompter()
client := &clcmd.Shell{
Renderer: clcmd.RendererJSON{Writer: writer},
AppFactory: clcmd.ChainlinkAppFactory{},
KeyStoreAuthenticator: clcmd.TerminalKeyStoreAuthenticator{Prompter: prompter},
FallbackAPIInitializer: clcmd.NewPromptingAPIInitializer(prompter),
Runner: clcmd.ChainlinkRunner{},
PromptingSessionRequestBuilder: clcmd.NewPromptingSessionRequestBuilder(prompter),
ChangePasswordPrompter: clcmd.NewChangePasswordPrompter(),
PasswordPrompter: clcmd.NewPasswordPrompter(),
}
app := clcmd.NewApp(client)
fs := flag.NewFlagSet("blah", flag.ContinueOnError)
fs.Bool("json", true, "")
fs.String("remote-node-url", remoteNodeURL, "")
helpers.PanicErr(app.Before(cli.NewContext(nil, fs, nil)))
// overwrite renderer since it's set to stdout after Before() is called
client.Renderer = clcmd.RendererJSON{Writer: writer}
return client, app
}

var (
checkMarkEmoji = "✅"
xEmoji = "❌"
Expand Down Expand Up @@ -142,7 +120,7 @@ func main() {
output := &bytes.Buffer{}
for key, node := range nodesMap {
node := node
client, app := connectToNode(&node.URL, output, node.CredsFile)
client, app := util.ConnectToNode(&node.URL, output, &node.CredsFile)
ethKeys := createETHKeysIfNeeded(client, app, output, numEthKeys, &node.URL, maxGasPriceGwei)
if key == model.VRFPrimaryNodeName {
vrfKeys := createVRFKeyIfNeeded(client, app, output, numVRFKeys, &node.URL)
Expand Down Expand Up @@ -242,7 +220,7 @@ func main() {

for key, node := range nodesMap {
node := node
client, app := connectToNode(&node.URL, output, node.CredsFile)
client, app := util.ConnectToNode(&node.URL, output, &node.CredsFile)

//GET ALL JOBS
jobIDs := getAllJobIDs(client, app, output)
Expand Down Expand Up @@ -291,7 +269,7 @@ func importVRFKeyToNodeIfSet(vrfBackupNodeURL *string, nodes map[string]model.No
vrfPrimaryNode := nodes[model.VRFBackupNodeName]

if len(vrfBackupNode.VrfKeys) == 0 || vrfPrimaryNode.VrfKeys[0] != vrfBackupNode.VrfKeys[0] {
client, app := connectToNode(&vrfBackupNode.URL, output, file)
client, app := util.ConnectToNode(&vrfBackupNode.URL, output, &file)
importVRFKey(client, app, output)

vrfKeys := getVRFKeys(client, app, output)
Expand Down Expand Up @@ -421,21 +399,6 @@ func printVRFKeyData(vrfKeys []presenters.VRFKeyResource) {
}
}

func connectToNode(nodeURL *string, output *bytes.Buffer, credFile string) (*clcmd.Shell, *cli.App) {
client, app := newApp(*nodeURL, output)
// login first to establish the session
fmt.Println("logging in to:", *nodeURL)
loginFs := flag.NewFlagSet("test", flag.ContinueOnError)
loginFs.String("file", credFile, "")
loginFs.Bool("bypass-version-check", true, "")
loginCtx := cli.NewContext(app, loginFs, nil)
err := client.RemoteLogin(loginCtx)
helpers.PanicErr(err)
output.Reset()
fmt.Println()
return client, app
}

func createVRFKeyIfNeeded(client *clcmd.Shell, app *cli.App, output *bytes.Buffer, numVRFKeys *int, nodeURL *string) []presenters.VRFKeyResource {
var allVRFKeys []presenters.VRFKeyResource
var newKeys []presenters.VRFKeyResource
Expand Down
48 changes: 48 additions & 0 deletions core/scripts/common/vrf/util/util.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,16 @@
package util

import (
"bytes"
"flag"
"fmt"
"io"

"github.com/urfave/cli"

helpers "github.com/smartcontractkit/chainlink/core/scripts/common"
"github.com/smartcontractkit/chainlink/core/scripts/common/vrf/model"
clcmd "github.com/smartcontractkit/chainlink/v2/core/cmd"
)

func MapToSendingKeyArr(nodeSendingKeys []string) []model.SendingKey {
Expand All @@ -20,3 +29,42 @@ func MapToAddressArr(sendingKeys []model.SendingKey) []string {
}
return sendingKeysString
}

func ConnectToNode(nodeURL *string, output *bytes.Buffer, credFile *string) (*clcmd.Shell, *cli.App) {
client, app := newApp(*nodeURL, output)
// login first to establish the session
fmt.Println("logging in to:", *nodeURL)
loginFs := flag.NewFlagSet("test", flag.ContinueOnError)
if credFile != nil {
loginFs.String("file", *credFile, "")
}
loginFs.Bool("bypass-version-check", true, "")
loginCtx := cli.NewContext(app, loginFs, nil)
err := client.RemoteLogin(loginCtx)
helpers.PanicErr(err)
output.Reset()
fmt.Println()
return client, app
}

func newApp(remoteNodeURL string, writer io.Writer) (*clcmd.Shell, *cli.App) {
prompter := clcmd.NewTerminalPrompter()
client := &clcmd.Shell{
Renderer: clcmd.RendererJSON{Writer: writer},
AppFactory: clcmd.ChainlinkAppFactory{},
KeyStoreAuthenticator: clcmd.TerminalKeyStoreAuthenticator{Prompter: prompter},
FallbackAPIInitializer: clcmd.NewPromptingAPIInitializer(prompter),
Runner: clcmd.ChainlinkRunner{},
PromptingSessionRequestBuilder: clcmd.NewPromptingSessionRequestBuilder(prompter),
ChangePasswordPrompter: clcmd.NewChangePasswordPrompter(),
PasswordPrompter: clcmd.NewPasswordPrompter(),
}
app := clcmd.NewApp(client)
fs := flag.NewFlagSet("blah", flag.ContinueOnError)
fs.Bool("json", true, "")
fs.String("remote-node-url", remoteNodeURL, "")
helpers.PanicErr(app.Before(cli.NewContext(nil, fs, nil)))
// overwrite renderer since it's set to stdout after Before() is called
client.Renderer = clcmd.RendererJSON{Writer: writer}
return client, app
}
122 changes: 122 additions & 0 deletions core/scripts/vrfv2/testnet/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@ package main
import (
"bytes"
"context"
"crypto/rand"
"encoding/base64"
"encoding/hex"
"flag"
"fmt"
"io"
"log"
"math/big"
"net/http"
"os"
"strings"

Expand All @@ -23,6 +27,8 @@ import (

"github.com/jmoiron/sqlx"

vrfutil "github.com/smartcontractkit/chainlink/core/scripts/common/vrf/util"

helpers "github.com/smartcontractkit/chainlink/core/scripts/common"
"github.com/smartcontractkit/chainlink/v2/core/chains/evm/assets"
evmtypes "github.com/smartcontractkit/chainlink/v2/core/chains/evm/types"
Expand All @@ -43,6 +49,7 @@ import (
"github.com/smartcontractkit/chainlink/v2/core/logger"
"github.com/smartcontractkit/chainlink/v2/core/services/blockhashstore"
"github.com/smartcontractkit/chainlink/v2/core/services/keystore"
"github.com/smartcontractkit/chainlink/v2/core/services/keystore/keys/vrfkey"
"github.com/smartcontractkit/chainlink/v2/core/services/pg"
"github.com/smartcontractkit/chainlink/v2/core/services/vrf/proof"
"github.com/smartcontractkit/chainlink/v2/core/utils"
Expand All @@ -56,6 +63,121 @@ func main() {
e := helpers.SetupEnv(false)

switch os.Args[1] {
// "generate-proof" generates a proof for a given request ID and block number
// using they VRF key from the given VRF node. It can optionally submit the
// transaction on-chain. This scripts is useful when you want to manually fulfill
// a request ID but do not already have a proof
// this script exports key from the given node and decrypts it in memory
// the exported key is not persisted locally
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this script only be executed somewhere in a safe environment, e.g. on the CLL infrastructure with direct access to the CL node? Just a thought of precaution, if you can execute this from your laptop, then the private key for proofs might be exposed to in-memory attacks (AVTs or advanced volatile threats). BTW what is the recovery scenario in case the private key for generating proofs ever gets hijacked from the CL node, can we replace it with another one?

case "generate-proof":
cmd := flag.NewFlagSet("generate-proof", flag.ExitOnError)
vrfNodeURL := cmd.String("vrf-node-url", "", "remote node URL")
pubKey := cmd.String("compressed-pub-key", "", "compressed public key")
requestBlock := cmd.Uint64("request-block", 0, "request block")
requestID := cmd.String("request-id", "", "request ID")
coordinatorAddress := cmd.String("coordinator-address", "", "request ID")
executeTx := cmd.Bool("execute-tx", false, "if true, posts fulfillment transaction on-chain")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What would be the use case for this script if you don't want to force fulfill the request? Just to generate the proof? Maybe we can call the script generate-proof-and-execute-tx (or similar) to be more clear.

helpers.ParseArgs(cmd, os.Args[2:], "vrf-node-url", "compressed-pub-key", "request-block", "request-id", "coordinator-address")

output := &bytes.Buffer{}
client, _ := vrfutil.ConnectToNode(vrfNodeURL, output, nil)

// generate password for encrypting the key
randomBytes := make([]byte, 32)
_, err := rand.Read(randomBytes)
helpers.PanicErr(err)
pw := base64.URLEncoding.EncodeToString(randomBytes)

fmt.Println("Exporting vrf key...")
resp, err := client.HTTP.Post("/v2/keys/vrf/export/"+*pubKey+"?newpassword="+pw, nil)
helpers.PanicErr(err)
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
panic(fmt.Sprintf("invalid status code: %d", resp.StatusCode))
}

keyJson, err := io.ReadAll(resp.Body)
helpers.PanicErr(err)

fmt.Println("Received vrf key")

key, err := vrfkey.FromEncryptedJSON(keyJson, pw)
helpers.PanicErr(err)

logs, err := e.Ec.FilterLogs(context.Background(), ethereum.FilterQuery{
FromBlock: big.NewInt(0).SetUint64(*requestBlock),
ToBlock: big.NewInt(0).SetUint64(*requestBlock),
Addresses: []common.Address{
common.HexToAddress(*coordinatorAddress),
},
Topics: [][]common.Hash{
{
vrf_coordinator_v2.VRFCoordinatorV2RandomWordsRequested{}.Topic(),
},
},
})
helpers.PanicErr(err)

fmt.Println("Searching for request log...")

coordinator, err := vrf_coordinator_v2.NewVRFCoordinatorV2(
common.HexToAddress(*coordinatorAddress),
e.Ec)
helpers.PanicErr(err)

var request *vrf_coordinator_v2.VRFCoordinatorV2RandomWordsRequested
for _, log := range logs {
unpacked, err2 := coordinator.ParseLog(log)
helpers.PanicErr(err2)
rwr, ok := unpacked.(*vrf_coordinator_v2.VRFCoordinatorV2RandomWordsRequested)
if !ok {
// should never happen
panic("cast to *vrf_coordinator_v2.VRFCoordinatorV2RandomWordsRequested")
}
if rwr.RequestId.String() == *requestID {
fmt.Printf("found request ID: %s\n", *requestID)
request = rwr
break
}
}

if request == nil {
panic(fmt.Sprintf("couldn't find request ID: %s in block: %d", *requestID, *requestBlock))
}

ps, err := proof.BigToSeed(request.PreSeed)
helpers.PanicErr(err)
preSeedData := proof.PreSeedDataV2{
PreSeed: ps,
BlockHash: request.Raw.BlockHash,
BlockNum: *requestBlock,
SubId: request.SubId,
CallbackGasLimit: request.CallbackGasLimit,
NumWords: request.NumWords,
Sender: request.Sender,
}

fmt.Printf("preseed data : %+v\n", preSeedData)
finalSeed := proof.FinalSeedV2(preSeedData)

p, err := key.GenerateProof(finalSeed)
helpers.PanicErr(err)

proof, rc, err := proof.GenerateProofResponseFromProofV2(p, preSeedData)
helpers.PanicErr(err)

fmt.Printf("Proof: %+v, commitment: %+v\n", proof, rc)

if *executeTx {
fmt.Println("Sending fulfillment!")
tx, err := coordinator.FulfillRandomWords(e.Owner, proof, rc)
helpers.PanicErr(err)

helpers.ConfirmTXMined(context.Background(), e.Ec, tx, e.ChainID, fmt.Sprintf("manual fulfillment of request ID: %s", *requestID))
}
fmt.Println("done!")

case "manual-fulfill":
cmd := flag.NewFlagSet("manual-fulfill", flag.ExitOnError)
// In order to get the tx data for a fulfillment transaction, you can grep the
Expand Down
Loading