package main import ( "context" "encoding/base64" "encoding/hex" "flag" "fmt" "io/ioutil" "log" "net/http" "strconv" "time" "github.com/certusone/wormhole/node/pkg/common" "github.com/certusone/wormhole/node/pkg/db" "github.com/wormhole-foundation/wormhole/sdk" "go.uber.org/zap" gossipv1 "github.com/certusone/wormhole/node/pkg/proto/gossip/v1" nodev1 "github.com/certusone/wormhole/node/pkg/proto/node/v1" "github.com/certusone/wormhole/node/pkg/watchers/cosmwasm" "github.com/wormhole-foundation/wormhole/sdk/vaa" "github.com/tidwall/gjson" "golang.org/x/time/rate" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" ) var fcdMap = map[vaa.ChainID]string{ vaa.ChainIDTerra: "https://fcd.terra.dev", vaa.ChainIDTerra2: "https://phoenix-fcd.terra.dev", vaa.ChainIDXpla: "https://dimension-fcd.xpla.dev", } var coreContractMap = map[vaa.ChainID]string{ vaa.ChainIDTerra: "terra1dq03ugtd40zu9hcgdzrsq6z2z4hwhc9tqk2uy5", vaa.ChainIDTerra2: "terra12mrnzvhx3rpej6843uge2yyfppfyd3u9c3uq223q8sl48huz9juqffcnhp", vaa.ChainIDXpla: "xpla1jn8qmdda5m6f6fqu9qv46rt7ajhklg40ukpqchkejcvy8x7w26cqxamv3w", } var emitterMap = map[vaa.ChainID]string{ vaa.ChainIDTerra: "0000000000000000000000007cf7b764e38a0a5e967972c1df77d432510564e2", vaa.ChainIDTerra2: "a463ad028fb79679cfc8ce1efba35ac0e77b35080a1abe9bebe83461f176b0a3", vaa.ChainIDXpla: "8f9cf727175353b17a5f574270e370776123d90fd74956ae4277962b4fdee24c", } type Emitter struct { ChainID vaa.ChainID Emitter string } var ( adminRPC = flag.String("adminRPC", "/run/guardiand/admin.socket", "Admin RPC address") chain = flag.String("chain", "terra", "CosmWasm Chain name") dryRun = flag.Bool("dryRun", true, "Dry run") sleepTime = flag.Int("sleepTime", 1, "Time to sleep between http requests") ) func getAdminClient(ctx context.Context, addr string) (*grpc.ClientConn, error, nodev1.NodePrivilegedServiceClient) { conn, err := grpc.DialContext(ctx, fmt.Sprintf("unix:///%s", addr), grpc.WithTransportCredentials(insecure.NewCredentials())) if err != nil { log.Fatalf("failed to connect to %s: %v", addr, err) } c := nodev1.NewNodePrivilegedServiceClient(conn) return conn, err, c } func getSequencesForTxhash(txhash string, fcd string, contractAddressLogKey string, coreContract string, emitter Emitter, chainID vaa.ChainID) ([]uint64, error) { client := &http.Client{ Timeout: time.Second * 5, } url := fmt.Sprintf("%s/cosmos/tx/v1beta1/txs/%s", fcd, txhash) resp, err := client.Get(url) if err != nil { return []uint64{}, fmt.Errorf("failed to get message: %w", err) } defer resp.Body.Close() txBody, err := ioutil.ReadAll(resp.Body) if err != nil { return []uint64{}, fmt.Errorf("failed to read message: %w", err) } txJSON := string(txBody) if !gjson.Valid(txJSON) { return []uint64{}, fmt.Errorf("invalid JSON response") } txHashRaw := gjson.Get(txJSON, "tx_response.txhash") if !txHashRaw.Exists() { return []uint64{}, fmt.Errorf("cosmwasm tx does not have tx hash") } txHash := txHashRaw.String() events := gjson.Get(txJSON, "tx_response.events") if !events.Exists() { return []uint64{}, fmt.Errorf("cosmwasm tx has no events") } msgs := EventsToMessagePublications(coreContract, txHash, events.Array(), chainID, contractAddressLogKey) // Should only ever be 1 message. Stole the above function from watcher.go var sequences = []uint64{} for _, msg := range msgs { tokenBridgeEmitter, err := vaa.StringToAddress(emitter.Emitter) if err != nil { log.Fatalf("Emitter address is not valid: %s", emitter.Emitter) } if msg.EmitterAddress == tokenBridgeEmitter { sequences = append(sequences, msg.Sequence) } } return sequences, nil } // This was stolen from pkg/cosmwasm/watcher.go func EventsToMessagePublications(contract string, txHash string, events []gjson.Result, chainID vaa.ChainID, contractAddressLogKey string) []*common.MessagePublication { msgs := make([]*common.MessagePublication, 0, len(events)) for _, event := range events { if !event.IsObject() { log.Println("event is invalid", zap.String("tx_hash", txHash), zap.String("event", event.String())) continue } eventType := gjson.Get(event.String(), "type") if eventType.String() != "wasm" { continue } attributes := gjson.Get(event.String(), "attributes") if !attributes.Exists() { log.Println("message event has no attributes", zap.String("tx_hash", txHash), zap.String("event", event.String())) continue } mappedAttributes := map[string]string{} for _, attribute := range attributes.Array() { if !attribute.IsObject() { log.Println("event attribute is invalid", zap.String("tx_hash", txHash), zap.String("attribute", attribute.String())) continue } keyBase := gjson.Get(attribute.String(), "key") if !keyBase.Exists() { log.Println("event attribute does not have key", zap.String("tx_hash", txHash), zap.String("attribute", attribute.String())) continue } valueBase := gjson.Get(attribute.String(), "value") if !valueBase.Exists() { log.Println("event attribute does not have value", zap.String("tx_hash", txHash), zap.String("attribute", attribute.String())) continue } key, err := base64.StdEncoding.DecodeString(keyBase.String()) if err != nil { log.Println("event key attribute is invalid", zap.String("tx_hash", txHash), zap.String("key", keyBase.String())) continue } value, err := base64.StdEncoding.DecodeString(valueBase.String()) if err != nil { log.Println("event value attribute is invalid", zap.String("tx_hash", txHash), zap.String("key", keyBase.String()), zap.String("value", valueBase.String())) continue } if _, ok := mappedAttributes[string(key)]; ok { log.Println("duplicate key in events", zap.String("tx_hash", txHash), zap.String("key", keyBase.String()), zap.String("value", valueBase.String())) continue } mappedAttributes[string(key)] = string(value) } contractAddress, ok := mappedAttributes[contractAddressLogKey] if !ok { log.Println("wasm event without contract address field set", zap.String("event", event.String())) continue } // This is not a wormhole message if contractAddress != contract { continue } payload, ok := mappedAttributes["message.message"] if !ok { log.Println("wormhole event does not have a message field", zap.String("tx_hash", txHash), zap.String("attributes", attributes.String())) continue } sender, ok := mappedAttributes["message.sender"] if !ok { log.Println("wormhole event does not have a sender field", zap.String("tx_hash", txHash), zap.String("attributes", attributes.String())) continue } nonce, ok := mappedAttributes["message.nonce"] if !ok { log.Println("wormhole event does not have a nonce field", zap.String("tx_hash", txHash), zap.String("attributes", attributes.String())) continue } sequence, ok := mappedAttributes["message.sequence"] if !ok { log.Println("wormhole event does not have a sequence field", zap.String("tx_hash", txHash), zap.String("attributes", attributes.String())) continue } blockTime, ok := mappedAttributes["message.block_time"] if !ok { log.Println("wormhole event does not have a block_time field", zap.String("tx_hash", txHash), zap.String("attributes", attributes.String())) continue } senderAddress, err := cosmwasm.StringToAddress(sender) if err != nil { log.Println("cannot decode emitter hex", zap.String("tx_hash", txHash), zap.String("value", sender)) continue } txHashValue, err := cosmwasm.StringToHash(txHash) if err != nil { log.Println("cannot decode tx hash hex", zap.String("tx_hash", txHash), zap.String("value", txHash)) continue } payloadValue, err := hex.DecodeString(payload) if err != nil { log.Println("cannot decode payload", zap.String("tx_hash", txHash), zap.String("value", payload)) continue } blockTimeInt, err := strconv.ParseInt(blockTime, 10, 64) if err != nil { log.Println("blocktime cannot be parsed as int", zap.String("tx_hash", txHash), zap.String("value", blockTime)) continue } nonceInt, err := strconv.ParseUint(nonce, 10, 32) if err != nil { log.Println("nonce cannot be parsed as int", zap.String("tx_hash", txHash), zap.String("value", blockTime)) continue } sequenceInt, err := strconv.ParseUint(sequence, 10, 64) if err != nil { log.Println("sequence cannot be parsed as int", zap.String("tx_hash", txHash), zap.String("value", blockTime)) continue } messagePublication := &common.MessagePublication{ TxHash: txHashValue, Timestamp: time.Unix(blockTimeInt, 0), Nonce: uint32(nonceInt), Sequence: sequenceInt, EmitterChain: chainID, EmitterAddress: senderAddress, Payload: payloadValue, ConsistencyLevel: 0, // Instant finality } msgs = append(msgs, messagePublication) } return msgs } func main() { flag.Parse() chainID, err := vaa.ChainIDFromString(*chain) if err != nil { log.Fatalf("Invalid chain: %v", err) } fcd, ok := fcdMap[chainID] if !ok { log.Fatal("Unsupported chain: no FCD defined") } coreContract, ok := coreContractMap[chainID] if !ok { log.Fatal("Unsupported chain: no core contract defined") } emitterAddress, ok := emitterMap[chainID] if !ok { log.Fatal("Unsupported chain: no emitter defined") } emitter := Emitter{chainID, emitterAddress} // CosmWasm 1.0.0 contractAddressLogKey := "_contract_address" if chainID == vaa.ChainIDTerra { // CosmWasm <1.0.0 contractAddressLogKey = "contract_address" } ctx := context.Background() missingMessages := make(map[uint64]bool) conn, err, admin := getAdminClient(ctx, *adminRPC) defer conn.Close() if err != nil { log.Fatalf("failed to get admin client: %v", err) } log.Printf("Requesting missing messages for %s", emitter.Emitter) msg := nodev1.FindMissingMessagesRequest{ EmitterChain: uint32(chainID), EmitterAddress: emitter.Emitter, RpcBackfill: true, BackfillNodes: sdk.PublicRPCEndpoints, } resp, err := admin.FindMissingMessages(ctx, &msg) if err != nil { log.Fatalf("failed to run find FindMissingMessages RPC: %v", err) } msgs := make([]*db.VAAID, len(resp.MissingMessages)) for i, id := range resp.MissingMessages { vId, err := db.VaaIDFromString(id) if err != nil { log.Fatalf("failed to parse VAAID: %v", err) } msgs[i] = vId } if len(msgs) == 0 { log.Printf("No missing messages found for %s", emitter) return } lowest := msgs[0].Sequence highest := msgs[len(msgs)-1].Sequence log.Printf("Found %d missing messages for %s: %d - %d", len(msgs), emitter, lowest, highest) for _, msg := range msgs { missingMessages[msg.Sequence] = true } limiter := rate.NewLimiter(rate.Every(time.Duration(*sleepTime)*time.Second), 1) log.Printf("Starting search for missing sequence numbers (sleeping %ds between requests)...", *sleepTime) offset := 0 var firstTime bool = true for (offset > 0) || firstTime { if err := limiter.Wait(ctx); err != nil { log.Fatalf("failed to wait: %v", err) } firstTime = false client := &http.Client{ Timeout: time.Second * 5, } resp, err := client.Get(fmt.Sprintf("%s/v1/txs?offset=%d&limit=100&account=%s", fcd, offset, coreContract)) if err != nil { log.Fatalf("failed to get log: %v", err) continue } defer resp.Body.Close() blocksBody, err := ioutil.ReadAll(resp.Body) if err != nil { log.Fatalf("failed to read log: %v", err) continue } blockJSON := string(blocksBody) if !gjson.Valid(blockJSON) { log.Println("invalid JSON response") continue } next := gjson.Get(blockJSON, "next") log.Println("next block", next.Int()) offset = int(next.Uint()) // Get the transactions. Should be 100 of them txs := gjson.Get(blockJSON, "txs") for _, tx := range txs.Array() { if !tx.IsObject() { log.Fatalln("Bad Object") continue } txhash := gjson.Get(tx.String(), "txhash") // Get sequence number for tx seqs, err := getSequencesForTxhash(txhash.String(), fcd, contractAddressLogKey, coreContract, emitter, chainID) if err != nil { log.Fatalln("Failed getting sequence number", err) continue } for _, seq := range seqs { // Check to see if this is a missing sequence number if !missingMessages[seq] { continue } log.Println("txhash", txhash.String(), "sequence number", seq) // send observation request to guardian if *dryRun { log.Println("Would have sent txhash", txhash, "to the guardian to re-observe") } else { txHashAsByteArray, err := cosmwasm.StringToHash(txhash.String()) if err != nil { log.Fatalln("Couldn't decode the txhash", txhash) } else { _, err = admin.SendObservationRequest(ctx, &nodev1.SendObservationRequestRequest{ ObservationRequest: &gossipv1.ObservationRequest{ ChainId: uint32(chainID), TxHash: txHashAsByteArray.Bytes(), }}) if err != nil { log.Fatalf("SendObservationRequest: %v", err) } } } if seq <= uint64(lowest) { // We are done log.Println("Finished!") return } } } } }