Node: Initial guardiand changes for accounting (#2181)
* node: guardiand support for accounting Change-Id: I97fe1f6d6d71a5803881ff4c793e3c30f22b14d8 * Node: Tie accounting into the guardian Change-Id: I31600d18176f516b75b3eb046fd7ac6e54e1b133 * Node: accounting tests and metrics Change-Id: Ieb139772edf464ed1ab202861babeaf0f857ad6b * Node: minor tweak to accounting metrics Change-Id: Iad2b7e34870734f0c5e5d538c0ac86269a9a4728 * Node: load accounting key Change-Id: I228ce23e63b556d751000b97097202eda48650aa * More work in progress Change-Id: I85088d26c05cf02d26043cf6ee8c67efd13f2ea4 * Node: send observations to accounting contract Change-Id: Ib90909c2ee705d5e2a7e6cf3a6ec4ba7519e2eb1 * Node: Fix lint error in accounting tests Change-Id: Id73397cf45107243a9f68ba82bed3ccf2b0299b5 * Node: Need to copy libwasmvm.so Change-Id: I2856c8964ca082f1f4014d6db9fb1b2dc4e64409 * Node: Rename wormchain to wormconn Change-Id: I6782be733ebdd92b908228d3984a906aa4c795f7 * Node: moving accounting check after governor Change-Id: I064c77d30514715c6f8b6b5da50806a5e1adf657 * Node: Add accounting status to heartbeat Change-Id: I0ae3e476386cfaccc5c877ee1351dbe41c0358c7 * Node: start of accounting integration work Change-Id: I8ad206eb7fc07aa9e1a2ebc321f2c490ec36b51e * Node: More broadcast tx stuff Change-Id: Id2cc83df859310c013665eaa9c6ce3033bb1d9c5 * Node: Can actually send a request to accounting Change-Id: I6af5d59c53939f58b2f13ae501914bef260592f2 * Node: More accounting tx broadcast stuff Change-Id: If758e49f8928807e87053320e9330c7208aad490 * Node: config changes for accounting Change-Id: I2803cceb188d04c557a52aa9aa8ba7296da8879f * Node: More accounting changes Change-Id: Id979af0ec6ab8484bc094072f3febf39355351ca * Node/Acct: Use new observation request format * Node/acct: use new contract interface * Node/acct: fix minor copy/paste error * Node: Clean up comments and lint errors * Node: disable accounting in dev by default * Node: Fix test failure * Remove test code * Switch messages to debug, rename Run() * check for "out of gas" * Use worker routine to submit observations * Rename mutex to reflect what it protects * Create handleEvents func * Remove FinalizeObservation * Node/Acct: Trying to use tm library for watcher * Node/acct: switch watcher to use tm library * Node/Acct: Need separate WS parm for accounting * Node/Acct: Fix compile error in tests * Node/Acct: Minor rework * Node: add wormchain as a dep to remove stale code * Node/Acct: GS index is not correct in requests * Node/Acct: Peg connection error metric * Node/Acct: Add wormchain to node docker file * Node/Acct: Fix for double base64 decode * Node/Acct: Change public key to sender address * Node/Acct: Fix lint error * Node/Acct: key pass phrase change * Node/Acct: Pass guardian index in obs req * Node/Acct: No go on submit observation * Node/Acct: Don't double encode tx_hash * Node/Acct: Remove unneeded base64 encoding * Node/Acct: handle submit channel overflow * Node/Acct: Added a TODO to document a review issue * Node/Acct: Fix for checking if channel is full Co-authored-by: Conor Patrick <conorpp94@gmail.com>
This commit is contained in:
parent
8777c22d32
commit
499c8424e4
1
Tiltfile
1
Tiltfile
|
@ -731,6 +731,7 @@ if wormchain:
|
|||
"guardian-validator",
|
||||
port_forwards = [
|
||||
port_forward(1319, container_port = 1317, name = "REST [:1319]", host = webHost),
|
||||
port_forward(9090, container_port = 9090, name = "GRPC", host = webHost),
|
||||
port_forward(26659, container_port = 26657, name = "TENDERMINT [:26659]", host = webHost)
|
||||
],
|
||||
resource_deps = [],
|
||||
|
|
|
@ -44,21 +44,32 @@ spec:
|
|||
# mount shared between containers for runtime state
|
||||
- name: node-rundir
|
||||
emptyDir: {}
|
||||
- name: node-keysdir
|
||||
- name: node-bigtable-key
|
||||
secret:
|
||||
secretName: node-bigtable-key
|
||||
optional: true
|
||||
items:
|
||||
- key: bigtable-key.json
|
||||
path: bigtable-key.json
|
||||
- name: node-wormchain-key
|
||||
secret:
|
||||
secretName: node-wormchain-key
|
||||
optional: false
|
||||
items:
|
||||
- key: wormchainKey0
|
||||
path: wormchainKey0
|
||||
- key: wormchainKey1
|
||||
path: wormchainKey1
|
||||
containers:
|
||||
- name: guardiand
|
||||
image: guardiand-image
|
||||
volumeMounts:
|
||||
- mountPath: /run/node
|
||||
name: node-rundir
|
||||
- mountPath: /tmp/mounted-keys
|
||||
name: node-keysdir
|
||||
- mountPath: /tmp/mounted-keys/bigtable
|
||||
name: node-bigtable-key
|
||||
- mountPath: /tmp/mounted-keys/wormchain
|
||||
name: node-wormchain-key
|
||||
command:
|
||||
- /guardiand
|
||||
- node
|
||||
|
@ -96,6 +107,17 @@ spec:
|
|||
# - ws://guardian-validator:26657/websocket
|
||||
# - --wormchainLCD
|
||||
# - http://guardian-validator:1317
|
||||
# - --wormchainURL
|
||||
# - guardian-validator:9090
|
||||
# - --wormchainKeyPath
|
||||
# - /tmp/mounted-keys/wormchain/wormchainKey
|
||||
# - --wormchainKeyPassPhrase
|
||||
# - test0000
|
||||
# - --accountingWS
|
||||
# - http://guardian-validator:26657
|
||||
# - --accountingContract
|
||||
# - wormhole1466nf3zuxpya8q9emxukd7vftaf6h4psr0a07srl5zw74zh84yjq4lyjmh
|
||||
# - --accountingCheckEnabled=true
|
||||
# - --terraWS
|
||||
# - ws://terra-terrad:26657/websocket
|
||||
# - --terraLCD
|
||||
|
@ -170,3 +192,12 @@ spec:
|
|||
- containerPort: 2345
|
||||
name: debugger
|
||||
protocol: TCP
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: node-wormchain-key
|
||||
type: Opaque
|
||||
data:
|
||||
wormchainKey0: LS0tLS1CRUdJTiBURU5ERVJNSU5UIFBSSVZBVEUgS0VZLS0tLS0Ka2RmOiBiY3J5cHQKc2FsdDogNDc2ODc2NkE3OEZEN0ZBQjMwMUJGOTM5MUYwQ0Y2M0YKdHlwZTogc2VjcDI1NmsxCgpkbEZuN1ZqRk02RnJjYkdaVDRWeE5yRlE3SUhQS2RyVVBCRTYraW8yK0w0VFZqcis5emNIQTF3dzNubWtqNVFlCnVSekJWMjQyeUdTc3hNTTJZckI2Q1ZXdzlaWXJJY3JFeks1c0FuST0KPXB2aHkKLS0tLS1FTkQgVEVOREVSTUlOVCBQUklWQVRFIEtFWS0tLS0t
|
||||
wormchainKey1: LS0tLS1CRUdJTiBURU5ERVJNSU5UIFBSSVZBVEUgS0VZLS0tLS0Ka2RmOiBiY3J5cHQKc2FsdDogMjMyRTU2NDMyMjBBNTcwRkVEQjFFMTFFOTNFM0E4NEIKdHlwZTogc2VjcDI1NmsxCgpBZjJ3aXNLdlBDOW4vaExYcDZaS1k5S091aVNYZG1lb3VvSzd3QVJ3cmNtTDV3MGs0YjFDSE5xTEp3ZXU1OEFGCkdTWGJsU3oySzNuWEl1V2hJZWtSNXE5WGRuUko4cGhSRWltbFNZST0KPU1vY1QKLS0tLS1FTkQgVEVOREVSTUlOVCBQUklWQVRFIEtFWS0tLS0tCg==
|
||||
|
|
|
@ -18,12 +18,15 @@ RUN --mount=type=cache,target=/root/.cache --mount=type=cache,target=/go \
|
|||
|
||||
COPY node node
|
||||
COPY sdk sdk
|
||||
COPY wormchain wormchain
|
||||
|
||||
ARG GO_BUILD_ARGS=-race
|
||||
|
||||
RUN --mount=type=cache,target=/root/.cache --mount=type=cache,target=/go \
|
||||
cd node && \
|
||||
go build ${GO_BUILD_ARGS} -gcflags="all=-N -l" --ldflags '-extldflags "-Wl,--allow-multiple-definition" -X "github.com/certusone/wormhole/node/cmd/guardiand.Build=dev"' -mod=readonly -o /guardiand github.com/certusone/wormhole/node
|
||||
go build ${GO_BUILD_ARGS} -gcflags="all=-N -l" --ldflags '-extldflags "-Wl,--allow-multiple-definition" -X "github.com/certusone/wormhole/node/cmd/guardiand.Build=dev"' -mod=readonly -o /guardiand github.com/certusone/wormhole/node && \
|
||||
go get github.com/CosmWasm/wasmvm@v1.0.0 && \
|
||||
cp /go/pkg/mod/github.com/!cosm!wasm/wasmvm@v1.0.0/api/libwasmvm.x86_64.so /usr/lib/
|
||||
|
||||
# Only export the final binary (+ shared objects). This reduces the image size
|
||||
# from ~1GB to ~150MB.
|
||||
|
@ -33,6 +36,7 @@ FROM scratch as export
|
|||
# have to copy all the dynamic libraries
|
||||
COPY --from=build /lib/* /lib/
|
||||
COPY --from=build /lib64/* /lib64/
|
||||
COPY --from=build /usr/lib/libwasmvm.x86_64.so /usr/lib/
|
||||
|
||||
# Copy the shells as entrypoints, but no utilities are necessary
|
||||
COPY --from=build /bin/bash /bin/dash /bin/sh /bin/
|
||||
|
|
|
@ -12,9 +12,9 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
ethcommon "github.com/ethereum/go-ethereum/common"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
ethcommon "github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/mr-tron/base58"
|
||||
"github.com/spf13/pflag"
|
||||
|
@ -210,7 +210,7 @@ func runSignWormchainValidatorAddress(cmd *cobra.Command, args []string) error {
|
|||
if err != nil {
|
||||
return fmt.Errorf("failed to load guardian key: %w", err)
|
||||
}
|
||||
addr, err := getFromBech32(wormchainAddress, "wormhole")
|
||||
addr, err := types.GetFromBech32(wormchainAddress, "wormhole")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to decode wormchain address: %w", err)
|
||||
}
|
||||
|
|
|
@ -1,285 +0,0 @@
|
|||
package guardiand
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// TODO(cpatrick): replace this whole file with just "github.com/cosmos/cosmos-sdk/types"
|
||||
// This is added to avoid changing go.mod for guardiand for now.
|
||||
|
||||
// The code in this file was faithfully copied from: https://github.com/scrtlabs/btcutil/blob/master/bech32/bech32.go
|
||||
// The code in this "bech32.go" file, and only this file, has the following license:
|
||||
/*
|
||||
ISC License
|
||||
|
||||
Copyright (c) 2013-2017 The btcsuite developers
|
||||
Copyright (c) 2016-2017 The Lightning Network Developers
|
||||
|
||||
Permission to use, copy, modify, and distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
*/
|
||||
|
||||
// toChars converts the byte slice 'data' to a string where each byte in 'data'
|
||||
// encodes the index of a character in 'charset'.
|
||||
func toChars(data []byte) (string, error) {
|
||||
result := make([]byte, 0, len(data))
|
||||
for _, b := range data {
|
||||
if int(b) >= len(charset) {
|
||||
return "", fmt.Errorf("invalid data byte: %v", b)
|
||||
}
|
||||
result = append(result, charset[b])
|
||||
}
|
||||
return string(result), nil
|
||||
}
|
||||
|
||||
// For more details on the checksum calculation, please refer to BIP 173.
|
||||
func bech32Checksum(hrp string, data []byte) []byte {
|
||||
// Convert the bytes to list of integers, as this is needed for the
|
||||
// checksum calculation.
|
||||
integers := make([]int, len(data))
|
||||
for i, b := range data {
|
||||
integers[i] = int(b)
|
||||
}
|
||||
values := append(bech32HrpExpand(hrp), integers...)
|
||||
values = append(values, []int{0, 0, 0, 0, 0, 0}...)
|
||||
polymod := bech32Polymod(values) ^ 1
|
||||
var res []byte
|
||||
for i := 0; i < 6; i++ {
|
||||
res = append(res, byte((polymod>>uint(5*(5-i)))&31))
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// For more details on the polymod calculation, please refer to BIP 173.
|
||||
func bech32Polymod(values []int) int {
|
||||
chk := 1
|
||||
for _, v := range values {
|
||||
b := chk >> 25
|
||||
chk = (chk&0x1ffffff)<<5 ^ v
|
||||
for i := 0; i < 5; i++ {
|
||||
if (b>>uint(i))&1 == 1 {
|
||||
chk ^= gen[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
return chk
|
||||
}
|
||||
|
||||
// For more details on HRP expansion, please refer to BIP 173.
|
||||
func bech32HrpExpand(hrp string) []int {
|
||||
v := make([]int, 0, len(hrp)*2+1)
|
||||
for i := 0; i < len(hrp); i++ {
|
||||
v = append(v, int(hrp[i]>>5))
|
||||
}
|
||||
v = append(v, 0)
|
||||
for i := 0; i < len(hrp); i++ {
|
||||
v = append(v, int(hrp[i]&31))
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// For more details on the checksum verification, please refer to BIP 173.
|
||||
func bech32VerifyChecksum(hrp string, data []byte) bool {
|
||||
integers := make([]int, len(data))
|
||||
for i, b := range data {
|
||||
integers[i] = int(b)
|
||||
}
|
||||
concat := append(bech32HrpExpand(hrp), integers...)
|
||||
return bech32Polymod(concat) == 1
|
||||
}
|
||||
|
||||
// toBytes converts each character in the string 'chars' to the value of the
|
||||
// index of the correspoding character in 'charset'.
|
||||
func toBytes(chars string) ([]byte, error) {
|
||||
decoded := make([]byte, 0, len(chars))
|
||||
for i := 0; i < len(chars); i++ {
|
||||
index := strings.IndexByte(charset, chars[i])
|
||||
if index < 0 {
|
||||
return nil, fmt.Errorf("invalid character not part of "+
|
||||
"charset: %v", chars[i])
|
||||
}
|
||||
decoded = append(decoded, byte(index))
|
||||
}
|
||||
return decoded, nil
|
||||
}
|
||||
|
||||
const charset = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
|
||||
|
||||
var gen = []int{0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3}
|
||||
|
||||
// Decode decodes a bech32 encoded string, returning the human-readable
|
||||
// part and the data part excluding the checksum.
|
||||
func decode(bech string, limit int) (string, []byte, error) {
|
||||
// The maximum allowed length for a bech32 string is 90. It must also
|
||||
// be at least 8 characters, since it needs a non-empty HRP, a
|
||||
// separator, and a 6 character checksum.
|
||||
if len(bech) < 8 || len(bech) > limit {
|
||||
return "", nil, fmt.Errorf("invalid bech32 string length %d",
|
||||
len(bech))
|
||||
}
|
||||
// Only ASCII characters between 33 and 126 are allowed.
|
||||
for i := 0; i < len(bech); i++ {
|
||||
if bech[i] < 33 || bech[i] > 126 {
|
||||
return "", nil, fmt.Errorf("invalid character in "+
|
||||
"string: '%c'", bech[i])
|
||||
}
|
||||
}
|
||||
|
||||
// The characters must be either all lowercase or all uppercase.
|
||||
lower := strings.ToLower(bech)
|
||||
upper := strings.ToUpper(bech)
|
||||
if bech != lower && bech != upper {
|
||||
return "", nil, fmt.Errorf("string not all lowercase or all " +
|
||||
"uppercase")
|
||||
}
|
||||
|
||||
// We'll work with the lowercase string from now on.
|
||||
bech = lower
|
||||
|
||||
// The string is invalid if the last '1' is non-existent, it is the
|
||||
// first character of the string (no human-readable part) or one of the
|
||||
// last 6 characters of the string (since checksum cannot contain '1'),
|
||||
// or if the string is more than 90 characters in total.
|
||||
one := strings.LastIndexByte(bech, '1')
|
||||
if one < 1 || one+7 > len(bech) {
|
||||
return "", nil, fmt.Errorf("invalid index of 1")
|
||||
}
|
||||
|
||||
// The human-readable part is everything before the last '1'.
|
||||
hrp := bech[:one]
|
||||
data := bech[one+1:]
|
||||
|
||||
// Each character corresponds to the byte with value of the index in
|
||||
// 'charset'.
|
||||
decoded, err := toBytes(data)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("failed converting data to bytes: "+
|
||||
"%v", err)
|
||||
}
|
||||
|
||||
if !bech32VerifyChecksum(hrp, decoded) {
|
||||
moreInfo := ""
|
||||
checksum := bech[len(bech)-6:]
|
||||
expected, err := toChars(bech32Checksum(hrp,
|
||||
decoded[:len(decoded)-6]))
|
||||
if err == nil {
|
||||
moreInfo = fmt.Sprintf("Expected %v, got %v.",
|
||||
expected, checksum)
|
||||
}
|
||||
return "", nil, fmt.Errorf("checksum failed. " + moreInfo)
|
||||
}
|
||||
|
||||
// We exclude the last 6 bytes, which is the checksum.
|
||||
return hrp, decoded[:len(decoded)-6], nil
|
||||
}
|
||||
|
||||
func decodeAndConvert(bech string) (string, []byte, error) {
|
||||
hrp, data, err := decode(bech, 1023)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("decoding bech32 failed: %w", err)
|
||||
}
|
||||
|
||||
converted, err := convertBits(data, 5, 8, false)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("decoding bech32 failed: %w", err)
|
||||
}
|
||||
|
||||
return hrp, converted, nil
|
||||
}
|
||||
|
||||
// ConvertBits converts a byte slice where each byte is encoding fromBits bits,
|
||||
// to a byte slice where each byte is encoding toBits bits.
|
||||
func convertBits(data []byte, fromBits, toBits uint8, pad bool) ([]byte, error) {
|
||||
if fromBits < 1 || fromBits > 8 || toBits < 1 || toBits > 8 {
|
||||
return nil, fmt.Errorf("only bit groups between 1 and 8 allowed")
|
||||
}
|
||||
|
||||
// The final bytes, each byte encoding toBits bits.
|
||||
var regrouped []byte
|
||||
|
||||
// Keep track of the next byte we create and how many bits we have
|
||||
// added to it out of the toBits goal.
|
||||
nextByte := byte(0)
|
||||
filledBits := uint8(0)
|
||||
|
||||
for _, b := range data {
|
||||
|
||||
// Discard unused bits.
|
||||
b = b << (8 - fromBits)
|
||||
|
||||
// How many bits remaining to extract from the input data.
|
||||
remFromBits := fromBits
|
||||
for remFromBits > 0 {
|
||||
// How many bits remaining to be added to the next byte.
|
||||
remToBits := toBits - filledBits
|
||||
|
||||
// The number of bytes to next extract is the minimum of
|
||||
// remFromBits and remToBits.
|
||||
toExtract := remFromBits
|
||||
if remToBits < toExtract {
|
||||
toExtract = remToBits
|
||||
}
|
||||
|
||||
// Add the next bits to nextByte, shifting the already
|
||||
// added bits to the left.
|
||||
nextByte = (nextByte << toExtract) | (b >> (8 - toExtract))
|
||||
|
||||
// Discard the bits we just extracted and get ready for
|
||||
// next iteration.
|
||||
b = b << toExtract
|
||||
remFromBits -= toExtract
|
||||
filledBits += toExtract
|
||||
|
||||
// If the nextByte is completely filled, we add it to
|
||||
// our regrouped bytes and start on the next byte.
|
||||
if filledBits == toBits {
|
||||
regrouped = append(regrouped, nextByte)
|
||||
filledBits = 0
|
||||
nextByte = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We pad any unfinished group if specified.
|
||||
if pad && filledBits > 0 {
|
||||
nextByte = nextByte << (toBits - filledBits)
|
||||
regrouped = append(regrouped, nextByte)
|
||||
filledBits = 0
|
||||
nextByte = 0
|
||||
}
|
||||
|
||||
// Any incomplete group must be <= 4 bits, and all zeroes.
|
||||
if filledBits > 0 && (filledBits > 4 || nextByte != 0) {
|
||||
return nil, fmt.Errorf("invalid incomplete group")
|
||||
}
|
||||
|
||||
return regrouped, nil
|
||||
}
|
||||
|
||||
// GetFromBech32 decodes a bytestring from a Bech32 encoded string.
|
||||
func getFromBech32(bech32str, prefix string) ([]byte, error) {
|
||||
if len(bech32str) == 0 {
|
||||
return nil, fmt.Errorf("zero length bech32 address")
|
||||
}
|
||||
|
||||
hrp, bz, err := decodeAndConvert(bech32str)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if hrp != prefix {
|
||||
return nil, fmt.Errorf("invalid Bech32 prefix; expected %s, got %s", prefix, hrp)
|
||||
}
|
||||
|
||||
return bz, nil
|
||||
}
|
|
@ -23,6 +23,7 @@ import (
|
|||
"github.com/certusone/wormhole/node/pkg/watchers/near"
|
||||
"github.com/certusone/wormhole/node/pkg/watchers/solana"
|
||||
"github.com/certusone/wormhole/node/pkg/watchers/sui"
|
||||
"github.com/certusone/wormhole/node/pkg/wormconn"
|
||||
|
||||
"github.com/benbjohnson/clock"
|
||||
"github.com/certusone/wormhole/node/pkg/db"
|
||||
|
@ -36,6 +37,7 @@ import (
|
|||
"github.com/gorilla/mux"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
|
||||
"github.com/certusone/wormhole/node/pkg/accounting"
|
||||
"github.com/certusone/wormhole/node/pkg/common"
|
||||
"github.com/certusone/wormhole/node/pkg/devnet"
|
||||
"github.com/certusone/wormhole/node/pkg/governor"
|
||||
|
@ -45,6 +47,7 @@ import (
|
|||
"github.com/certusone/wormhole/node/pkg/readiness"
|
||||
"github.com/certusone/wormhole/node/pkg/reporter"
|
||||
"github.com/certusone/wormhole/node/pkg/supervisor"
|
||||
cosmoscrypto "github.com/cosmos/cosmos-sdk/crypto/types"
|
||||
eth_common "github.com/ethereum/go-ethereum/common"
|
||||
ethcrypto "github.com/ethereum/go-ethereum/crypto"
|
||||
"github.com/libp2p/go-libp2p/core/crypto"
|
||||
|
@ -139,8 +142,15 @@ var (
|
|||
nearRPC *string
|
||||
nearContract *string
|
||||
|
||||
wormchainWS *string
|
||||
wormchainLCD *string
|
||||
wormchainWS *string
|
||||
wormchainLCD *string
|
||||
wormchainURL *string
|
||||
wormchainKeyPath *string
|
||||
wormchainKeyPassPhrase *string // TODO Is there a better way to do this??
|
||||
|
||||
accountingContract *string
|
||||
accountingWS *string
|
||||
accountingCheckEnabled *bool
|
||||
|
||||
aptosRPC *string
|
||||
aptosAccount *string
|
||||
|
@ -280,6 +290,13 @@ func init() {
|
|||
|
||||
wormchainWS = NodeCmd.Flags().String("wormchainWS", "", "Path to wormchaind root for websocket connection")
|
||||
wormchainLCD = NodeCmd.Flags().String("wormchainLCD", "", "Path to LCD service root for http calls")
|
||||
wormchainURL = NodeCmd.Flags().String("wormchainURL", "", "wormhole-chain gRPC URL")
|
||||
wormchainKeyPath = NodeCmd.Flags().String("wormchainKeyPath", "", "path to wormhole-chain private key for signing transactions")
|
||||
wormchainKeyPassPhrase = NodeCmd.Flags().String("wormchainKeyPassPhrase", "", "pass phrase used to unarmor the wormchain key file")
|
||||
|
||||
accountingWS = NodeCmd.Flags().String("accountingWS", "", "Websocket used to listen to the accounting smart contract on wormchain")
|
||||
accountingContract = NodeCmd.Flags().String("accountingContract", "", "Address of the accounting smart contract on wormchain")
|
||||
accountingCheckEnabled = NodeCmd.Flags().Bool("accountingCheckEnabled", false, "Should accounting be enforced on transfers")
|
||||
|
||||
aptosRPC = NodeCmd.Flags().String("aptosRPC", "", "aptos RPC URL")
|
||||
aptosAccount = NodeCmd.Flags().String("aptosAccount", "", "aptos account")
|
||||
|
@ -902,6 +919,92 @@ func runNode(cmd *cobra.Command, args []string) {
|
|||
// provides methods for reporting progress toward message attestation, and channels for receiving attestation lifecyclye events.
|
||||
attestationEvents := reporter.EventListener(logger)
|
||||
|
||||
// If the wormchain sending info is configured, connect to it.
|
||||
var wormchainKey cosmoscrypto.PrivKey
|
||||
var wormchainConn *wormconn.ClientConn
|
||||
if *wormchainURL != "" {
|
||||
if *wormchainKeyPath == "" {
|
||||
logger.Fatal("if wormchainURL is specified, wormchainKeyPath is required")
|
||||
}
|
||||
|
||||
if *wormchainKeyPassPhrase == "" {
|
||||
logger.Fatal("if wormchainURL is specified, wormchainKeyPassPhrase is required")
|
||||
}
|
||||
|
||||
// Load the wormchain key.
|
||||
wormchainKeyPathName := *wormchainKeyPath
|
||||
if *unsafeDevMode {
|
||||
idx, err := devnet.GetDevnetIndex()
|
||||
if err != nil {
|
||||
logger.Fatal("failed to get devnet index", zap.Error(err))
|
||||
}
|
||||
wormchainKeyPathName = fmt.Sprint(*wormchainKeyPath, idx)
|
||||
}
|
||||
|
||||
logger.Debug("acct: loading key file", zap.String("key path", wormchainKeyPathName))
|
||||
wormchainKey, err = wormconn.LoadWormchainPrivKey(wormchainKeyPathName, *wormchainKeyPassPhrase)
|
||||
if err != nil {
|
||||
logger.Fatal("failed to load devnet wormchain private key", zap.Error(err))
|
||||
}
|
||||
|
||||
// Connect to wormchain.
|
||||
logger.Info("Connecting to wormchain", zap.String("wormchainURL", *wormchainURL), zap.String("wormchainKeyPath", wormchainKeyPathName))
|
||||
wormchainConn, err = wormconn.NewConn(rootCtx, *wormchainURL, wormchainKey)
|
||||
if err != nil {
|
||||
logger.Fatal("failed to connect to wormchain", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
// Set up accounting. If the accounting smart contract is configured, we will instantiate accounting and VAAs
|
||||
// will be passed to it for processing. It will forward all token bridge transfers to the accounting contract.
|
||||
// If accountingCheckEnabled is set to true, token bridge transfers will not be signed and published until they
|
||||
// are approved by the accounting smart contract.
|
||||
|
||||
// TODO: Use this once PR #1931 is merged.
|
||||
//acctReadC, acctWriteC := makeChannelPair[*common.MessagePublication](0)
|
||||
acctChan := make(chan *common.MessagePublication)
|
||||
var acctReadC <-chan *common.MessagePublication = acctChan
|
||||
var acctWriteC chan<- *common.MessagePublication = acctChan
|
||||
|
||||
var acct *accounting.Accounting
|
||||
if *accountingContract != "" {
|
||||
if *accountingWS == "" {
|
||||
logger.Fatal("acct: if accountingContract is specified, accountingWS is required")
|
||||
}
|
||||
if *wormchainLCD == "" {
|
||||
logger.Fatal("acct: if accountingContract is specified, wormchainLCD is required")
|
||||
}
|
||||
if wormchainConn == nil {
|
||||
logger.Fatal("acct: if accountingContract is specified, the wormchain sending connection must be enabled")
|
||||
}
|
||||
if *accountingCheckEnabled {
|
||||
logger.Info("acct: accounting is enabled and will be enforced")
|
||||
} else {
|
||||
logger.Info("acct: accounting is enabled but will not be enforced")
|
||||
}
|
||||
env := accounting.MainNetMode
|
||||
if *testnetMode {
|
||||
env = accounting.TestNetMode
|
||||
} else if *unsafeDevMode {
|
||||
env = accounting.DevNetMode
|
||||
}
|
||||
acct = accounting.NewAccounting(
|
||||
rootCtx,
|
||||
logger,
|
||||
db,
|
||||
*accountingContract,
|
||||
*accountingWS,
|
||||
wormchainConn,
|
||||
*accountingCheckEnabled,
|
||||
gk,
|
||||
gst,
|
||||
acctWriteC,
|
||||
env,
|
||||
)
|
||||
} else {
|
||||
logger.Info("acct: accounting is disabled")
|
||||
}
|
||||
|
||||
var gov *governor.ChainGovernor
|
||||
if *chainGovernorEnabled {
|
||||
logger.Info("chain governor is enabled")
|
||||
|
@ -925,7 +1028,7 @@ func runNode(cmd *cobra.Command, args []string) {
|
|||
// Run supervisor.
|
||||
supervisor.New(rootCtx, logger, func(ctx context.Context) error {
|
||||
if err := supervisor.Run(ctx, "p2p", p2p.Run(
|
||||
obsvC, obsvReqC, obsvReqSendC, sendC, signedInC, priv, gk, gst, *p2pPort, *p2pNetworkID, *p2pBootstrap, *nodeName, *disableHeartbeatVerify, rootCtxCancel, gov, nil, nil)); err != nil {
|
||||
obsvC, obsvReqC, obsvReqSendC, sendC, signedInC, priv, gk, gst, *p2pPort, *p2pNetworkID, *p2pBootstrap, *nodeName, *disableHeartbeatVerify, rootCtxCancel, acct, gov, nil, nil)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -1227,6 +1330,12 @@ func runNode(cmd *cobra.Command, args []string) {
|
|||
}
|
||||
go handleReobservationRequests(rootCtx, clock.New(), logger, obsvReqC, chainObsvReqC)
|
||||
|
||||
if acct != nil {
|
||||
if err := acct.Start(ctx); err != nil {
|
||||
logger.Fatal("acct: failed to start accounting", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
if gov != nil {
|
||||
err := gov.Run(ctx)
|
||||
if err != nil {
|
||||
|
@ -1252,6 +1361,8 @@ func runNode(cmd *cobra.Command, args []string) {
|
|||
attestationEvents,
|
||||
notifier,
|
||||
gov,
|
||||
acct,
|
||||
acctReadC,
|
||||
)
|
||||
if err := supervisor.Run(ctx, "processor", p.Run); err != nil {
|
||||
return err
|
||||
|
|
|
@ -530,7 +530,7 @@ func runSpy(cmd *cobra.Command, args []string) {
|
|||
|
||||
// Run supervisor.
|
||||
supervisor.New(rootCtx, logger, func(ctx context.Context) error {
|
||||
if err := supervisor.Run(ctx, "p2p", p2p.Run(obsvC, obsvReqC, nil, sendC, signedInC, priv, nil, gst, *p2pPort, *p2pNetworkID, *p2pBootstrap, "", false, rootCtxCancel, nil, nil, nil)); err != nil {
|
||||
if err := supervisor.Run(ctx, "p2p", p2p.Run(obsvC, obsvReqC, nil, sendC, signedInC, priv, nil, gst, *p2pPort, *p2pNetworkID, *p2pBootstrap, "", false, rootCtxCancel, nil, nil, nil, nil)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
149
node/go.mod
149
node/go.mod
|
@ -5,8 +5,8 @@ go 1.19
|
|||
require (
|
||||
cloud.google.com/go/bigtable v1.10.1
|
||||
github.com/celo-org/celo-blockchain v1.5.5
|
||||
github.com/cenkalti/backoff/v4 v4.1.1
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e
|
||||
github.com/cenkalti/backoff/v4 v4.1.3
|
||||
github.com/coreos/go-systemd v0.0.0-20190620071333-e64a0ec8b42a
|
||||
github.com/davecgh/go-spew v1.1.1
|
||||
github.com/dgraph-io/badger/v3 v3.2103.1
|
||||
github.com/diamondburned/arikawa/v3 v3.0.0-rc.2
|
||||
|
@ -17,7 +17,7 @@ require (
|
|||
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.5.0
|
||||
github.com/improbable-eng/grpc-web v0.14.1
|
||||
github.com/improbable-eng/grpc-web v0.15.0
|
||||
github.com/ipfs/go-log/v2 v2.5.1
|
||||
github.com/libp2p/go-libp2p v0.22.0
|
||||
github.com/libp2p/go-libp2p-kad-dht v0.18.0
|
||||
|
@ -27,100 +27,139 @@ require (
|
|||
github.com/mr-tron/base58 v1.2.0
|
||||
github.com/multiformats/go-multiaddr v0.6.0
|
||||
github.com/near/borsh-go v0.3.0
|
||||
github.com/prometheus/client_golang v1.12.1
|
||||
github.com/spf13/cobra v1.2.1
|
||||
github.com/prometheus/client_golang v1.12.2
|
||||
github.com/spf13/cobra v1.6.0
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/spf13/viper v1.8.1
|
||||
github.com/spf13/viper v1.13.0
|
||||
github.com/status-im/keycard-go v0.0.0-20200402102358-957c09536969
|
||||
github.com/stretchr/testify v1.8.0
|
||||
github.com/tendermint/tendermint v0.34.14
|
||||
github.com/stretchr/testify v1.8.1
|
||||
github.com/tendermint/tendermint v0.34.24
|
||||
github.com/tidwall/gjson v1.14.3
|
||||
go.uber.org/zap v1.23.0
|
||||
golang.org/x/crypto v0.1.0
|
||||
golang.org/x/sys v0.1.0
|
||||
golang.org/x/crypto v0.2.0
|
||||
golang.org/x/sys v0.2.0
|
||||
golang.org/x/time v0.0.0-20220609170525-579cf78fd858
|
||||
google.golang.org/api v0.99.0
|
||||
google.golang.org/genproto v0.0.0-20221018160656-63c7b68cfc55
|
||||
google.golang.org/api v0.102.0
|
||||
google.golang.org/genproto v0.0.0-20221114212237-e4508ebdbee1
|
||||
google.golang.org/grpc v1.50.1
|
||||
google.golang.org/protobuf v1.28.1
|
||||
google.golang.org/protobuf v1.28.2-0.20220831092852-f930b1dc76e8
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go/logging v1.4.2
|
||||
cloud.google.com/go/pubsub v1.25.1
|
||||
github.com/CosmWasm/wasmd v0.28.0
|
||||
github.com/algorand/go-algorand-sdk v1.23.0
|
||||
github.com/benbjohnson/clock v1.3.0
|
||||
github.com/blendle/zapdriver v1.3.1
|
||||
github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce
|
||||
github.com/cosmos/cosmos-sdk v0.45.9
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d
|
||||
github.com/test-go/testify v1.1.4
|
||||
github.com/wormhole-foundation/wormhole/sdk v0.0.0-00010101000000-000000000000
|
||||
golang.org/x/exp v0.0.0-20220426173459-3bcf042a4bf5
|
||||
github.com/wormhole-foundation/wormchain v0.0.0-00010101000000-000000000000
|
||||
github.com/wormhole-foundation/wormhole/sdk v0.0.0-20220926172624-4b38dc650bb0
|
||||
golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e
|
||||
golang.org/x/text v0.4.0
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.104.0 // indirect
|
||||
cloud.google.com/go/compute v1.10.0 // indirect
|
||||
cloud.google.com/go/iam v0.5.0 // indirect
|
||||
cloud.google.com/go v0.105.0 // indirect
|
||||
cloud.google.com/go/compute v1.12.1 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.2.1 // indirect
|
||||
cloud.google.com/go/iam v0.7.0 // indirect
|
||||
cloud.google.com/go/longrunning v0.3.0 // indirect
|
||||
contrib.go.opencensus.io/exporter/stackdriver v0.13.14 // indirect
|
||||
filippo.io/edwards25519 v1.0.0 // indirect
|
||||
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect
|
||||
github.com/99designs/keyring v1.2.1 // indirect
|
||||
github.com/ChainSafe/go-schnorrkel v0.0.0-20200405005733-88cbf1b4c40d // indirect
|
||||
github.com/CosmWasm/wasmvm v1.0.0 // indirect
|
||||
github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6 // indirect
|
||||
github.com/VictoriaMetrics/fastcache v1.6.0 // indirect
|
||||
github.com/Workiva/go-datastructures v1.0.53 // indirect
|
||||
github.com/algorand/go-codec/codec v1.1.8 // indirect
|
||||
github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 // indirect
|
||||
github.com/armon/go-metrics v0.4.0 // indirect
|
||||
github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bgentry/speakeasy v0.1.0 // indirect
|
||||
github.com/btcsuite/btcd v0.22.1 // indirect
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.2.0 // indirect
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 // indirect
|
||||
github.com/celo-org/celo-bls-go v0.2.4 // indirect
|
||||
github.com/cespare/xxhash v1.1.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
github.com/cheekybits/genny v1.0.0 // indirect
|
||||
github.com/coinbase/rosetta-sdk-go v0.7.0 // indirect
|
||||
github.com/confio/ics23/go v0.9.0 // indirect
|
||||
github.com/containerd/cgroups v1.0.4 // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.3.2 // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534 // indirect
|
||||
github.com/cosmos/btcutil v1.0.5 // indirect
|
||||
github.com/cosmos/go-bip39 v1.0.0 // indirect
|
||||
github.com/cosmos/gorocksdb v1.2.0 // indirect
|
||||
github.com/cosmos/iavl v0.19.3 // indirect
|
||||
github.com/cosmos/ibc-go/v3 v3.3.0 // indirect
|
||||
github.com/cosmos/ledger-cosmos-go v0.12.1 // indirect
|
||||
github.com/creachadair/taskgroup v0.3.2 // indirect
|
||||
github.com/danieljoos/wincred v1.1.2 // indirect
|
||||
github.com/davidlazar/go-crypto v0.0.0-20200604182044-b73af7476f6c // indirect
|
||||
github.com/deckarep/golang-set v1.8.0 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect
|
||||
github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f // indirect
|
||||
github.com/dfuse-io/logging v0.0.0-20210109005628-b97a57253f70 // indirect
|
||||
github.com/dgraph-io/badger/v2 v2.2007.4 // indirect
|
||||
github.com/dgraph-io/ristretto v0.1.0 // indirect
|
||||
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect
|
||||
github.com/docker/go-units v0.4.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.0 // indirect
|
||||
github.com/dvsekhvalnov/jose2go v1.5.0 // indirect
|
||||
github.com/elastic/gosigar v0.14.2 // indirect
|
||||
github.com/fatih/color v1.13.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.2 // indirect
|
||||
github.com/flynn/noise v1.0.0 // indirect
|
||||
github.com/francoispqt/gojay v1.2.13 // indirect
|
||||
github.com/fsnotify/fsnotify v1.5.4 // indirect
|
||||
github.com/gagliardetto/binary v0.7.3 // indirect
|
||||
github.com/gagliardetto/treeout v0.1.4 // indirect
|
||||
github.com/go-ole/go-ole v1.2.5 // indirect
|
||||
github.com/go-kit/kit v0.12.0 // indirect
|
||||
github.com/go-kit/log v0.2.1 // indirect
|
||||
github.com/go-logfmt/logfmt v0.5.1 // indirect
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
github.com/go-stack/stack v1.8.0 // indirect
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
|
||||
github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect
|
||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||
github.com/gogo/gateway v1.1.0 // indirect
|
||||
github.com/gogo/protobuf v1.3.3 // indirect
|
||||
github.com/golang/glog v0.0.0-20210429001901-424d2337a529 // indirect
|
||||
github.com/golang/glog v1.0.0 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/btree v1.0.1 // indirect
|
||||
github.com/google/flatbuffers v1.12.0 // indirect
|
||||
github.com/google/go-cmp v0.5.9 // indirect
|
||||
github.com/google/go-querystring v1.0.0 // indirect
|
||||
github.com/google/gofuzz v1.2.0 // indirect
|
||||
github.com/google/gopacket v1.1.19 // indirect
|
||||
github.com/google/orderedcode v0.0.1 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.2.0 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.6.0 // indirect
|
||||
github.com/gorilla/handlers v1.5.1 // indirect
|
||||
github.com/gorilla/schema v1.2.0 // indirect
|
||||
github.com/hashicorp/errwrap v1.0.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect
|
||||
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect
|
||||
github.com/gtank/merlin v0.1.1 // indirect
|
||||
github.com/gtank/ristretto255 v0.1.2 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/hdevalence/ed25519consensus v0.1.0 // indirect
|
||||
github.com/holiman/bloomfilter/v2 v2.0.3 // indirect
|
||||
github.com/holiman/uint256 v1.2.0 // indirect
|
||||
github.com/huin/goupnp v1.0.3 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.1 // indirect
|
||||
github.com/ipfs/go-cid v0.2.0 // indirect
|
||||
github.com/ipfs/go-datastore v0.5.1 // indirect
|
||||
github.com/ipfs/go-ipfs-util v0.0.2 // indirect
|
||||
|
@ -130,10 +169,12 @@ require (
|
|||
github.com/jackpal/go-nat-pmp v1.0.2 // indirect
|
||||
github.com/jbenet/go-temp-err-catcher v0.1.0 // indirect
|
||||
github.com/jbenet/goprocess v0.1.4 // indirect
|
||||
github.com/jmhodges/levigo v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/compress v1.15.11 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.1.0 // indirect
|
||||
github.com/koron/go-ssdp v0.0.3 // indirect
|
||||
github.com/lib/pq v1.10.6 // indirect
|
||||
github.com/libp2p/go-buffer-pool v0.1.0 // indirect
|
||||
github.com/libp2p/go-cidranger v1.1.0 // indirect
|
||||
github.com/libp2p/go-flow-metrics v0.1.0 // indirect
|
||||
|
@ -149,7 +190,7 @@ require (
|
|||
github.com/libp2p/go-yamux/v3 v3.1.2 // indirect
|
||||
github.com/logrusorgru/aurora v2.0.3+incompatible // indirect
|
||||
github.com/lucas-clemente/quic-go v0.28.1 // indirect
|
||||
github.com/magiconair/properties v1.8.5 // indirect
|
||||
github.com/magiconair/properties v1.8.6 // indirect
|
||||
github.com/marten-seemann/qtls-go1-16 v0.1.5 // indirect
|
||||
github.com/marten-seemann/qtls-go1-17 v0.1.2 // indirect
|
||||
github.com/marten-seemann/qtls-go1-18 v0.1.2 // indirect
|
||||
|
@ -159,16 +200,19 @@ require (
|
|||
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||
github.com/mattn/go-pointer v0.0.1 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.9 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
|
||||
github.com/miekg/dns v1.1.50 // indirect
|
||||
github.com/mikioh/tcpinfo v0.0.0-20190314235526-30a79bb1804b // indirect
|
||||
github.com/mikioh/tcpopt v0.0.0-20190314235656-172688c1accc // indirect
|
||||
github.com/mimoo/StrobeGo v0.0.0-20210601165009-122bf33a46e0 // indirect
|
||||
github.com/minio/highwayhash v1.0.2 // indirect
|
||||
github.com/minio/sha256-simd v1.0.0 // indirect
|
||||
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
|
||||
github.com/mitchellh/mapstructure v1.4.2 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1 // indirect
|
||||
github.com/mtibben/percent v0.2.1 // indirect
|
||||
github.com/multiformats/go-base32 v0.0.4 // indirect
|
||||
github.com/multiformats/go-base36 v0.1.0 // indirect
|
||||
github.com/multiformats/go-multiaddr-dns v0.3.1 // indirect
|
||||
|
@ -181,11 +225,12 @@ require (
|
|||
github.com/nxadm/tail v1.4.8 // indirect
|
||||
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
||||
github.com/onsi/ginkgo v1.16.5 // indirect
|
||||
github.com/onsi/gomega v1.13.0 // indirect
|
||||
github.com/onsi/gomega v1.19.0 // indirect
|
||||
github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417 // indirect
|
||||
github.com/opentracing/opentracing-go v1.2.0 // indirect
|
||||
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect
|
||||
github.com/pelletier/go-toml v1.9.4 // indirect
|
||||
github.com/pelletier/go-toml v1.9.5 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.5 // indirect
|
||||
github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
|
@ -194,46 +239,58 @@ require (
|
|||
github.com/prometheus/common v0.37.0 // indirect
|
||||
github.com/prometheus/procfs v0.8.0 // indirect
|
||||
github.com/prometheus/tsdb v0.7.1 // indirect
|
||||
github.com/rakyll/statik v0.1.7 // indirect
|
||||
github.com/raulk/go-watchdog v1.3.0 // indirect
|
||||
github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect
|
||||
github.com/regen-network/cosmos-proto v0.3.1 // indirect
|
||||
github.com/rjeczalik/notify v0.9.1 // indirect
|
||||
github.com/rs/cors v1.7.0 // indirect
|
||||
github.com/sasha-s/go-deadlock v0.2.1-0.20190427202633-1595213edefa // indirect
|
||||
github.com/rs/cors v1.8.2 // indirect
|
||||
github.com/rs/zerolog v1.27.0 // indirect
|
||||
github.com/sasha-s/go-deadlock v0.3.1 // indirect
|
||||
github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible // indirect
|
||||
github.com/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572 // indirect
|
||||
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
||||
github.com/spf13/afero v1.6.0 // indirect
|
||||
github.com/spf13/cast v1.3.1 // indirect
|
||||
github.com/spf13/afero v1.8.2 // indirect
|
||||
github.com/spf13/cast v1.5.0 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/streamingfast/logging v0.0.0-20220813175024-b4fbb0e893df // indirect
|
||||
github.com/subosito/gotenv v1.2.0 // indirect
|
||||
github.com/subosito/gotenv v1.4.1 // indirect
|
||||
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect
|
||||
github.com/tendermint/btcd v0.1.1 // indirect
|
||||
github.com/tendermint/crypto v0.0.0-20191022145703-50d29ede1e15 // indirect
|
||||
github.com/tendermint/go-amino v0.16.0 // indirect
|
||||
github.com/tendermint/spm v0.1.9 // indirect
|
||||
github.com/tendermint/tm-db v0.6.7 // indirect
|
||||
github.com/teris-io/shortid v0.0.0-20220617161101-71ec9f2aa569 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.5 // indirect
|
||||
github.com/tklauser/numcpus v0.2.2 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.10 // indirect
|
||||
github.com/tklauser/numcpus v0.4.0 // indirect
|
||||
github.com/tyler-smith/go-bip39 v1.0.2 // indirect
|
||||
github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1 // indirect
|
||||
github.com/whyrusleeping/timecache v0.0.0-20160911033111-cfcb2f1abfee // indirect
|
||||
github.com/zondax/hid v0.9.1 // indirect
|
||||
github.com/zondax/ledger-go v0.14.0 // indirect
|
||||
go.etcd.io/bbolt v1.3.6 // indirect
|
||||
go.opencensus.io v0.23.0 // indirect
|
||||
go.uber.org/atomic v1.10.0 // indirect
|
||||
go.uber.org/multierr v1.8.0 // indirect
|
||||
go.uber.org/ratelimit v0.2.0 // indirect
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
|
||||
golang.org/x/net v0.1.0 // indirect
|
||||
golang.org/x/mod v0.6.0 // indirect
|
||||
golang.org/x/net v0.2.0 // indirect
|
||||
golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783 // indirect
|
||||
golang.org/x/sync v0.1.0 // indirect
|
||||
golang.org/x/term v0.1.0 // indirect
|
||||
golang.org/x/tools v0.1.12 // indirect
|
||||
golang.org/x/term v0.2.0 // indirect
|
||||
golang.org/x/tools v0.2.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
gopkg.in/ini.v1 v1.63.2 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/natefinch/npipe.v2 v2.0.0-20160621034901-c1b8fa8bdcce // indirect
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
lukechampine.com/blake3 v1.1.7 // indirect
|
||||
nhooyr.io/websocket v1.8.6 // indirect
|
||||
nhooyr.io/websocket v1.8.7 // indirect
|
||||
)
|
||||
|
||||
// Needed for cosmos-sdk based chains. See
|
||||
|
@ -241,3 +298,9 @@ require (
|
|||
replace github.com/gogo/protobuf => github.com/regen-network/protobuf v1.3.3-alpha.regen.1
|
||||
|
||||
replace github.com/wormhole-foundation/wormhole/sdk => ../sdk
|
||||
|
||||
replace github.com/wormhole-foundation/wormchain => ../wormchain
|
||||
|
||||
replace github.com/CosmWasm/wasmd v0.28.0 => github.com/wormhole-foundation/wasmd v0.28.0-wormhole-2
|
||||
|
||||
replace github.com/cosmos/cosmos-sdk => github.com/wormhole-foundation/cosmos-sdk v0.45.9-wormhole
|
||||
|
|
2442
node/go.sum
2442
node/go.sum
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,8 @@
|
|||
-----BEGIN WORMHOLE GUARDIAN PRIVATE KEY-----
|
||||
PublicKey: 0xbeFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBe
|
||||
Description: auto-generated deterministic devnet key
|
||||
|
||||
CiDPsSMDoZzeWAu03XcWObDSa8aDU2RVcajP9RarLuEToBAB
|
||||
=VN/A
|
||||
-----END WORMHOLE GUARDIAN PRIVATE KEY-----
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
-----BEGIN TENDERMINT PRIVATE KEY-----
|
||||
kdf: bcrypt
|
||||
salt: 4768766A78FD7FAB301BF9391F0CF63F
|
||||
type: secp256k1
|
||||
|
||||
dlFn7VjFM6FrcbGZT4VxNrFQ7IHPKdrUPBE6+io2+L4TVjr+9zcHA1ww3nmkj5Qe
|
||||
uRzBV242yGSsxMM2YrB6CVWw9ZYrIcrEzK5sAnI=
|
||||
=pvhy
|
||||
-----END TENDERMINT PRIVATE KEY-----
|
||||
|
|
@ -0,0 +1,211 @@
|
|||
// This tool can be used to confirm that the CoinkGecko price query still works after the token list is updated.
|
||||
// Usage: go run check_query.go
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/certusone/wormhole/node/pkg/accounting"
|
||||
"github.com/certusone/wormhole/node/pkg/common"
|
||||
nodev1 "github.com/certusone/wormhole/node/pkg/proto/node/v1"
|
||||
"github.com/certusone/wormhole/node/pkg/wormconn"
|
||||
"github.com/wormhole-foundation/wormhole/sdk/vaa"
|
||||
|
||||
ethCrypto "github.com/ethereum/go-ethereum/crypto"
|
||||
|
||||
"golang.org/x/crypto/openpgp/armor" //nolint
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ctx := context.Background()
|
||||
logger, _ := zap.NewDevelopment()
|
||||
|
||||
// data, err := hex.DecodeString("C3AE4256EAA0BA6D01041585F63AE7CAA69D6D33")
|
||||
// if err != nil {
|
||||
// logger.Fatal("failed to hex decode string", zap.Error(err))
|
||||
// }
|
||||
|
||||
// conv, err := bech32.ConvertBits(data, 8, 5, true)
|
||||
// if err != nil {
|
||||
// logger.Fatal("failed to convert bits", zap.Error(err))
|
||||
// }
|
||||
|
||||
// encoded, err := bech32.Encode("wormhole", conv)
|
||||
// if err != nil {
|
||||
// logger.Fatal("bech32 encode failed", zap.Error(err))
|
||||
// }
|
||||
// logger.Info("encoded", zap.String("str", encoded))
|
||||
// return
|
||||
|
||||
wormchainURL := string("localhost:9090")
|
||||
wormchainKeyPath := string("./dev.wormchain.key")
|
||||
contract := "wormhole1466nf3zuxpya8q9emxukd7vftaf6h4psr0a07srl5zw74zh84yjq4lyjmh"
|
||||
guardianKeyPath := string("./dev.guardian.key")
|
||||
|
||||
wormchainKey, err := wormconn.LoadWormchainPrivKey(wormchainKeyPath, "test0000")
|
||||
if err != nil {
|
||||
logger.Fatal("failed to load devnet wormchain private key", zap.Error(err))
|
||||
}
|
||||
|
||||
wormchainConn, err := wormconn.NewConn(ctx, wormchainURL, wormchainKey)
|
||||
if err != nil {
|
||||
logger.Fatal("failed to connect to wormchain", zap.Error(err))
|
||||
}
|
||||
|
||||
logger.Info("Connected to wormchain",
|
||||
zap.String("wormchainURL", wormchainURL),
|
||||
zap.String("wormchainKeyPath", wormchainKeyPath),
|
||||
zap.String("senderAddress", wormchainConn.SenderAddress()),
|
||||
)
|
||||
|
||||
logger.Info("Loading guardian key", zap.String("guardianKeyPath", guardianKeyPath))
|
||||
gk, err := loadGuardianKey(guardianKeyPath)
|
||||
if err != nil {
|
||||
logger.Fatal("failed to load guardian key", zap.Error(err))
|
||||
}
|
||||
|
||||
sequence := uint64(time.Now().Unix())
|
||||
timestamp := time.Now()
|
||||
|
||||
if !testSubmit(ctx, logger, gk, wormchainConn, contract, "0000000000000000000000000290fb167208af455bb137780163b7b7a9a10c16", timestamp, sequence, false, false, "Submit should succeed") {
|
||||
return
|
||||
}
|
||||
|
||||
if !testSubmit(ctx, logger, gk, wormchainConn, contract, "0000000000000000000000000290fb167208af455bb137780163b7b7a9a10c16", timestamp, sequence, true, false, "Already commited should succeed") {
|
||||
return
|
||||
}
|
||||
|
||||
sequence += 1
|
||||
if !testSubmit(ctx, logger, gk, wormchainConn, contract, "0000000000000000000000000290fb167208af455bb137780163b7b7a9a10c17", timestamp, sequence, false, true, "Bad emitter address should fail") {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func testSubmit(
|
||||
ctx context.Context,
|
||||
logger *zap.Logger,
|
||||
gk *ecdsa.PrivateKey,
|
||||
wormchainConn *wormconn.ClientConn,
|
||||
contract string,
|
||||
emitterAddressStr string,
|
||||
timestamp time.Time,
|
||||
sequence uint64,
|
||||
expectedResult bool,
|
||||
errorExpected bool,
|
||||
tag string,
|
||||
) bool {
|
||||
EmitterAddress, _ := vaa.StringToAddress(emitterAddressStr)
|
||||
TxHash, _ := vaa.StringToHash("82ea2536c5d1671830cb49120f94479e34b54596a8dd369fbc2666667a765f4b")
|
||||
Payload, _ := hex.DecodeString("010000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000002d8be6bf0baa74e0a907016679cae9190e80dd0a0002000000000000000000000000c10820983f33456ce7beb3a046f5a83fa34f027d0c200000000000000000000000000000000000000000000000000000000000000000")
|
||||
gsIndex := uint32(0)
|
||||
guardianIndex := uint32(0)
|
||||
|
||||
msg := common.MessagePublication{
|
||||
TxHash: TxHash,
|
||||
Timestamp: timestamp,
|
||||
Nonce: uint32(0),
|
||||
Sequence: sequence,
|
||||
EmitterChain: vaa.ChainIDEthereum,
|
||||
EmitterAddress: EmitterAddress,
|
||||
ConsistencyLevel: uint8(15),
|
||||
Payload: Payload,
|
||||
}
|
||||
|
||||
txResp, err := accounting.SubmitObservationToContract(ctx, logger, gk, gsIndex, guardianIndex, wormchainConn, contract, &msg)
|
||||
if err != nil {
|
||||
logger.Error("acct: failed to broadcast Observation request", zap.String("test", tag), zap.Error(err))
|
||||
return false
|
||||
}
|
||||
|
||||
// out, err := wormchainConn.BroadcastTxResponseToString(txResp)
|
||||
// if err != nil {
|
||||
// logger.Error("acct: failed to parse broadcast response", zap.Error(err))
|
||||
// return false
|
||||
// }
|
||||
|
||||
alreadyCommitted, err := accounting.CheckSubmitObservationResult(txResp)
|
||||
if err != nil {
|
||||
if !errorExpected {
|
||||
logger.Error("acct: unexpected error", zap.String("test", tag), zap.Error(err))
|
||||
return false
|
||||
}
|
||||
|
||||
logger.Info("test succeeded, expected error returned", zap.String("test", tag), zap.Error(err))
|
||||
return true
|
||||
}
|
||||
if alreadyCommitted != expectedResult {
|
||||
out, err := wormchainConn.BroadcastTxResponseToString(txResp)
|
||||
if err != nil {
|
||||
logger.Error("acct: failed to parse broadcast response", zap.String("test", tag), zap.Error(err))
|
||||
return false
|
||||
}
|
||||
|
||||
logger.Info("test failed", zap.String("test", tag), zap.Uint64("seqNo", sequence), zap.Bool("alreadyCommitted", alreadyCommitted), zap.String("response", out))
|
||||
return false
|
||||
}
|
||||
|
||||
logger.Info("test succeeded", zap.String("test", tag))
|
||||
return true
|
||||
}
|
||||
|
||||
const (
|
||||
GuardianKeyArmoredBlock = "WORMHOLE GUARDIAN PRIVATE KEY"
|
||||
)
|
||||
|
||||
// loadGuardianKey loads a serialized guardian key from disk.
|
||||
func loadGuardianKey(filename string) (*ecdsa.PrivateKey, error) {
|
||||
f, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open file: %w", err)
|
||||
}
|
||||
|
||||
p, err := armor.Decode(f)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read armored file: %w", err)
|
||||
}
|
||||
|
||||
if p.Type != GuardianKeyArmoredBlock {
|
||||
return nil, fmt.Errorf("invalid block type: %s", p.Type)
|
||||
}
|
||||
|
||||
b, err := io.ReadAll(p.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read file: %w", err)
|
||||
}
|
||||
|
||||
var m nodev1.GuardianKey
|
||||
err = proto.Unmarshal(b, &m)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to deserialize protobuf: %w", err)
|
||||
}
|
||||
|
||||
gk, err := ethCrypto.ToECDSA(m.Data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to deserialize raw key data: %w", err)
|
||||
}
|
||||
|
||||
return gk, nil
|
||||
}
|
||||
|
||||
/*
|
||||
DEBUG: obs: {
|
||||
key: {
|
||||
emitter_chain: 2,
|
||||
emitter_address: 'AAAAAAAAAAAAAAAAApD7FnIIr0VbsTd4AWO3t6mhDBY=',
|
||||
sequence: 0
|
||||
},
|
||||
nonce: 0,
|
||||
payload: 'AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA3gtrOnZAAAAAAAAAAAAAAAAAAALYvmvwuqdOCpBwFmecrpGQ6A3QoAAgAAAAAAAAAAAAAAAMEIIJg/M0Vs576zoEb1qD+jTwJ9DCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==',
|
||||
tx_hash: '82ea2536c5d1671830cb49120f94479e34b54596a8dd369fbc2666667a765f4b'
|
||||
}
|
||||
*/
|
|
@ -0,0 +1,354 @@
|
|||
// The accounting package manages the interface to the accounting smart contract on wormchain. It is passed all VAAs before
|
||||
// they are signed and published. It determines if the VAA is for a token bridge transfer, and if it is, it submits an observation
|
||||
// request to the accounting contract. When that happens, the VAA is queued up until the accounting contract responds indicating
|
||||
// that the VAA has been approved. If the VAA is approved, this module will forward the VAA back to the processor loop to be signed
|
||||
// and published.
|
||||
|
||||
package accounting
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/certusone/wormhole/node/pkg/common"
|
||||
"github.com/certusone/wormhole/node/pkg/db"
|
||||
"github.com/certusone/wormhole/node/pkg/supervisor"
|
||||
"github.com/certusone/wormhole/node/pkg/wormconn"
|
||||
"github.com/wormhole-foundation/wormhole/sdk"
|
||||
"github.com/wormhole-foundation/wormhole/sdk/vaa"
|
||||
|
||||
ethCommon "github.com/ethereum/go-ethereum/common"
|
||||
ethCrypto "github.com/ethereum/go-ethereum/crypto"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
const (
|
||||
MainNetMode = 1
|
||||
TestNetMode = 2
|
||||
DevNetMode = 3
|
||||
GoTestMode = 4
|
||||
|
||||
// We will retry requests once per minute for up to an hour.
|
||||
auditInterval = time.Duration(time.Minute)
|
||||
maxRetries = 60
|
||||
)
|
||||
|
||||
type (
|
||||
// tokenBridgeKey is the key to the map of token bridges being monitored
|
||||
tokenBridgeKey struct {
|
||||
emitterChainId vaa.ChainID
|
||||
emitterAddr vaa.Address
|
||||
}
|
||||
|
||||
// tokenBridgeEntry is the payload of the map of the token bridges being monitored
|
||||
tokenBridgeEntry struct {
|
||||
}
|
||||
|
||||
// pendingEntry is the payload for each pending transfer
|
||||
pendingEntry struct {
|
||||
msg *common.MessagePublication
|
||||
msgId string
|
||||
digest string
|
||||
updTime time.Time
|
||||
retryCount int
|
||||
}
|
||||
)
|
||||
|
||||
// Accounting is the object that manages the interface to the wormchain accounting smart contract.
|
||||
type Accounting struct {
|
||||
ctx context.Context
|
||||
logger *zap.Logger
|
||||
db db.AccountingDB
|
||||
contract string
|
||||
wsUrl string
|
||||
wormchainConn *wormconn.ClientConn
|
||||
enforceFlag bool
|
||||
gk *ecdsa.PrivateKey
|
||||
gst *common.GuardianSetState
|
||||
guardianAddr ethCommon.Address
|
||||
msgChan chan<- *common.MessagePublication
|
||||
tokenBridges map[tokenBridgeKey]*tokenBridgeEntry
|
||||
pendingTransfersLock sync.Mutex
|
||||
pendingTransfers map[string]*pendingEntry // Key is the message ID (emitterChain/emitterAddr/seqNo)
|
||||
subChan chan *common.MessagePublication
|
||||
env int
|
||||
}
|
||||
|
||||
const subChanSize = 50
|
||||
|
||||
// NewAccounting creates a new instance of the Accounting object.
|
||||
func NewAccounting(
|
||||
ctx context.Context,
|
||||
logger *zap.Logger,
|
||||
db db.AccountingDB,
|
||||
contract string, // the address of the smart contract on wormchain
|
||||
wsUrl string, // the URL of the wormchain websocket interface
|
||||
wormchainConn *wormconn.ClientConn, // used for communicating with the smart contract
|
||||
enforceFlag bool, // whether or not accounting should be enforced
|
||||
gk *ecdsa.PrivateKey, // the guardian key used for signing observation requests
|
||||
gst *common.GuardianSetState, // used to get the current guardian set index when sending observation requests
|
||||
msgChan chan<- *common.MessagePublication, // the channel where transfers received by the accounting runnable should be published
|
||||
env int, // Controls the set of token bridges to be monitored
|
||||
) *Accounting {
|
||||
return &Accounting{
|
||||
ctx: ctx,
|
||||
logger: logger,
|
||||
db: db,
|
||||
contract: contract,
|
||||
wsUrl: wsUrl,
|
||||
wormchainConn: wormchainConn,
|
||||
enforceFlag: enforceFlag,
|
||||
gk: gk,
|
||||
gst: gst,
|
||||
guardianAddr: ethCrypto.PubkeyToAddress(gk.PublicKey),
|
||||
msgChan: msgChan,
|
||||
tokenBridges: make(map[tokenBridgeKey]*tokenBridgeEntry),
|
||||
pendingTransfers: make(map[string]*pendingEntry),
|
||||
subChan: make(chan *common.MessagePublication, subChanSize),
|
||||
env: env,
|
||||
}
|
||||
}
|
||||
|
||||
// Run initializes the accounting module and starts the watcher runnable.
|
||||
func (acct *Accounting) Start(ctx context.Context) error {
|
||||
acct.logger.Debug("acct: entering run")
|
||||
acct.pendingTransfersLock.Lock()
|
||||
defer acct.pendingTransfersLock.Unlock()
|
||||
|
||||
emitterMap := sdk.KnownTokenbridgeEmitters
|
||||
if acct.env == TestNetMode {
|
||||
emitterMap = sdk.KnownTestnetTokenbridgeEmitters
|
||||
} else if acct.env == DevNetMode || acct.env == GoTestMode {
|
||||
emitterMap = sdk.KnownDevnetTokenbridgeEmitters
|
||||
}
|
||||
|
||||
// Build the map of token bridges to be monitored.
|
||||
for chainId, emitterAddrBytes := range emitterMap {
|
||||
emitterAddr, err := vaa.BytesToAddress(emitterAddrBytes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to convert emitter address for chain: %v", chainId)
|
||||
}
|
||||
|
||||
tbk := tokenBridgeKey{emitterChainId: chainId, emitterAddr: emitterAddr}
|
||||
_, exists := acct.tokenBridges[tbk]
|
||||
if exists {
|
||||
return fmt.Errorf("detected duplicate token bridge for chain: %v", chainId)
|
||||
}
|
||||
|
||||
tbe := &tokenBridgeEntry{}
|
||||
acct.tokenBridges[tbk] = tbe
|
||||
acct.logger.Info("acct: will monitor token bridge:", zap.Stringer("emitterChainId", tbk.emitterChainId), zap.Stringer("emitterAddr", tbk.emitterAddr))
|
||||
}
|
||||
|
||||
// Load any existing pending transfers from the db.
|
||||
if err := acct.loadPendingTransfers(); err != nil {
|
||||
return fmt.Errorf("failed to load pending transfers from the db: %w", err)
|
||||
}
|
||||
|
||||
// Start the watcher to listen to transfer events from the smart contract.
|
||||
if acct.env != GoTestMode {
|
||||
if err := supervisor.Run(ctx, "acctworker", acct.worker); err != nil {
|
||||
return fmt.Errorf("failed to start submit observation worker: %w", err)
|
||||
}
|
||||
|
||||
if err := supervisor.Run(ctx, "acctwatcher", acct.watcher); err != nil {
|
||||
return fmt.Errorf("failed to start watcher: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (acct *Accounting) Close() {
|
||||
if acct.wormchainConn != nil {
|
||||
acct.wormchainConn.Close()
|
||||
acct.wormchainConn = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (acct *Accounting) FeatureString() string {
|
||||
if !acct.enforceFlag {
|
||||
return "acct:logonly"
|
||||
}
|
||||
return "acct:enforced"
|
||||
}
|
||||
|
||||
// SubmitObservation will submit token bridge transfers to the accounting smart contract. This is called from the processor
|
||||
// loop when a local observation is received from a watcher. It returns true if the observation can be published immediately,
|
||||
// false if not (because it has been submitted to accounting).
|
||||
func (acct *Accounting) SubmitObservation(msg *common.MessagePublication) (bool, error) {
|
||||
msgId := msg.MessageIDString()
|
||||
acct.logger.Debug("acct: in SubmitObservation", zap.String("msgID", msgId))
|
||||
// We only care about token bridges.
|
||||
tbk := tokenBridgeKey{emitterChainId: msg.EmitterChain, emitterAddr: msg.EmitterAddress}
|
||||
if _, exists := acct.tokenBridges[tbk]; !exists {
|
||||
if msg.EmitterChain != vaa.ChainIDPythNet {
|
||||
acct.logger.Debug("acct: ignoring vaa because it is not a token bridge", zap.String("msgID", msgId))
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// We only care about transfers.
|
||||
if !vaa.IsTransfer(msg.Payload) {
|
||||
acct.logger.Info("acct: ignoring vaa because it is not a transfer", zap.String("msgID", msgId))
|
||||
return true, nil
|
||||
}
|
||||
|
||||
digest := msg.CreateDigest()
|
||||
|
||||
acct.pendingTransfersLock.Lock()
|
||||
defer acct.pendingTransfersLock.Unlock()
|
||||
|
||||
// If this is already pending, don't send it again.
|
||||
if oldEntry, exists := acct.pendingTransfers[msgId]; exists {
|
||||
if oldEntry.digest != digest {
|
||||
digestMismatches.Inc()
|
||||
acct.logger.Error("acct: digest in pending transfer has changed, dropping it",
|
||||
zap.String("msgID", msgId),
|
||||
zap.String("oldDigest", oldEntry.digest),
|
||||
zap.String("newDigest", digest),
|
||||
)
|
||||
} else {
|
||||
acct.logger.Info("acct: blocking transfer because it is already outstanding", zap.String("msgID", msgId))
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Add it to the pending map and the database.
|
||||
if err := acct.addPendingTransfer(msgId, msg, digest); err != nil {
|
||||
acct.logger.Error("acct: failed to persist pending transfer, blocking publishing", zap.String("msgID", msgId), zap.Error(err))
|
||||
return false, err
|
||||
}
|
||||
|
||||
// This transaction may take a while. Pass it off to the worker so we don't block the processor.
|
||||
if acct.env != GoTestMode {
|
||||
acct.logger.Info("acct: submitting transfer to accounting for approval", zap.String("msgID", msgId), zap.Bool("canPublish", !acct.enforceFlag))
|
||||
acct.submitObservation(msg)
|
||||
}
|
||||
|
||||
// If we are not enforcing accounting, the event can be published. Otherwise we have to wait to hear back from the contract.
|
||||
return !acct.enforceFlag, nil
|
||||
}
|
||||
|
||||
// AuditPending audits the set of pending transfers for any that have been in the pending state too long. This is called from the processor loop
|
||||
// each timer interval. Any transfers that have been in the pending state too long will be resubmitted. Any that has been retried too many times
|
||||
// will be logged and dropped.
|
||||
func (acct *Accounting) AuditPendingTransfers() {
|
||||
acct.logger.Debug("acct: in AuditPendingTransfers")
|
||||
acct.pendingTransfersLock.Lock()
|
||||
defer acct.pendingTransfersLock.Unlock()
|
||||
|
||||
if len(acct.pendingTransfers) == 0 {
|
||||
acct.logger.Debug("acct: leaving AuditPendingTransfers, no pending transfers")
|
||||
return
|
||||
}
|
||||
|
||||
for msgId, pe := range acct.pendingTransfers {
|
||||
acct.logger.Debug("acct: evaluating pending transfer", zap.String("msgID", msgId), zap.Stringer("updTime", pe.updTime))
|
||||
if time.Since(pe.updTime) > auditInterval {
|
||||
pe.retryCount += 1
|
||||
if pe.retryCount > maxRetries {
|
||||
acct.logger.Error("acct: stuck pending transfer has reached the retry limit, dropping it", zap.String("msgId", msgId))
|
||||
acct.deletePendingTransfer(msgId)
|
||||
continue
|
||||
}
|
||||
|
||||
acct.logger.Error("acct: resubmitting pending transfer",
|
||||
zap.String("msgId", msgId),
|
||||
zap.Stringer("lastUpdateTime", pe.updTime),
|
||||
zap.Int("retryCount", pe.retryCount),
|
||||
)
|
||||
|
||||
pe.updTime = time.Now()
|
||||
acct.submitObservation(pe.msg)
|
||||
}
|
||||
}
|
||||
|
||||
acct.logger.Debug("acct: leaving AuditPendingTransfers")
|
||||
}
|
||||
|
||||
// publishTransfer publishes a pending transfer to the accounting channel and updates the timestamp. It assumes the caller holds the lock.
|
||||
func (acct *Accounting) publishTransfer(pe *pendingEntry) {
|
||||
if acct.enforceFlag {
|
||||
acct.logger.Debug("acct: publishTransfer: notifying the processor", zap.String("msgId", pe.msgId))
|
||||
acct.msgChan <- pe.msg
|
||||
}
|
||||
|
||||
acct.deletePendingTransfer(pe.msgId)
|
||||
}
|
||||
|
||||
// addPendingTransfer adds a pending transfer to both the map and the database. It assumes the caller holds the lock.
|
||||
func (acct *Accounting) addPendingTransfer(msgId string, msg *common.MessagePublication, digest string) error {
|
||||
acct.logger.Debug("acct: addPendingTransfer", zap.String("msgId", msgId))
|
||||
if err := acct.db.AcctStorePendingTransfer(msg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pe := &pendingEntry{msg: msg, msgId: msgId, digest: digest, updTime: time.Now()}
|
||||
acct.pendingTransfers[msgId] = pe
|
||||
transfersOutstanding.Inc()
|
||||
return nil
|
||||
}
|
||||
|
||||
// deletePendingTransfer deletes the transfer from both the map and the database. It assumes the caller holds the lock.
|
||||
func (acct *Accounting) deletePendingTransfer(msgId string) {
|
||||
acct.logger.Debug("acct: deletePendingTransfer", zap.String("msgId", msgId))
|
||||
if _, exists := acct.pendingTransfers[msgId]; exists {
|
||||
transfersOutstanding.Dec()
|
||||
delete(acct.pendingTransfers, msgId)
|
||||
}
|
||||
if err := acct.db.AcctDeletePendingTransfer(msgId); err != nil {
|
||||
acct.logger.Error("acct: failed to delete pending transfer from the db", zap.String("msgId", msgId), zap.Error(err))
|
||||
// Ignore this error and keep going.
|
||||
}
|
||||
}
|
||||
|
||||
// loadPendingTransfers loads any pending transfers that are present in the database. This method assumes the caller holds the lock.
|
||||
func (acct *Accounting) loadPendingTransfers() error {
|
||||
pendingTransfers, err := acct.db.AcctGetData(acct.logger)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, msg := range pendingTransfers {
|
||||
msgId := msg.MessageIDString()
|
||||
acct.logger.Info("acct: reloaded pending transfer", zap.String("msgID", msgId))
|
||||
|
||||
digest := msg.CreateDigest()
|
||||
pe := &pendingEntry{msg: msg, msgId: msgId, digest: digest} // Leave the updTime unset so we will query this on the first audit interval.
|
||||
acct.pendingTransfers[msgId] = pe
|
||||
transfersOutstanding.Inc()
|
||||
}
|
||||
|
||||
if len(acct.pendingTransfers) != 0 {
|
||||
acct.logger.Info("acct: reloaded pending transfers", zap.Int("total", len(acct.pendingTransfers)))
|
||||
} else {
|
||||
acct.logger.Info("acct: no pending transfers to be reloaded")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// submitObservation sends an observation request to the worker so it can be submited to the contract.
|
||||
// If writing to the channel would block, this function resets the timestamp on the entry so it will be
|
||||
// retried next audit interval. This method assumes the caller holds the lock.
|
||||
func (acct *Accounting) submitObservation(msg *common.MessagePublication) {
|
||||
select {
|
||||
case acct.subChan <- msg:
|
||||
acct.logger.Debug("acct: submitted observation to channel", zap.String("msgId", msg.MessageIDString()))
|
||||
default:
|
||||
msgId := msg.MessageIDString()
|
||||
acct.logger.Error("acct: unable to submit observation because the channel is full, will try next interval", zap.String("msgId", msgId))
|
||||
pe, exists := acct.pendingTransfers[msgId]
|
||||
if exists {
|
||||
pe.updTime = time.Time{}
|
||||
} else {
|
||||
acct.logger.Error("acct: failed to look up pending transfer", zap.String("msgId", msgId))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,242 @@
|
|||
package accounting
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"math/big"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
ethCommon "github.com/ethereum/go-ethereum/common"
|
||||
ethCrypto "github.com/ethereum/go-ethereum/crypto"
|
||||
|
||||
"github.com/certusone/wormhole/node/pkg/common"
|
||||
"github.com/certusone/wormhole/node/pkg/db"
|
||||
"github.com/certusone/wormhole/node/pkg/devnet"
|
||||
"github.com/wormhole-foundation/wormhole/sdk/vaa"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
const (
|
||||
enforceAccounting = true
|
||||
dontEnforceAccounting = false
|
||||
)
|
||||
|
||||
func newAccountingForTest(
|
||||
t *testing.T,
|
||||
ctx context.Context,
|
||||
accountingCheckEnabled bool,
|
||||
acctWriteC chan<- *common.MessagePublication,
|
||||
) *Accounting {
|
||||
logger := zap.NewNop()
|
||||
var db db.MockAccountingDB
|
||||
|
||||
gk := devnet.InsecureDeterministicEcdsaKeyByIndex(ethCrypto.S256(), uint64(0))
|
||||
|
||||
gst := common.NewGuardianSetState(nil)
|
||||
gs := &common.GuardianSet{}
|
||||
gst.Set(gs)
|
||||
|
||||
acct := NewAccounting(
|
||||
ctx,
|
||||
logger,
|
||||
&db,
|
||||
"0xdeadbeef", // accountingContract
|
||||
"none", // accountingWS
|
||||
nil, // wormchainConn
|
||||
accountingCheckEnabled,
|
||||
gk,
|
||||
gst,
|
||||
acctWriteC,
|
||||
GoTestMode,
|
||||
)
|
||||
|
||||
err := acct.Start(ctx)
|
||||
require.NoError(t, err)
|
||||
return acct
|
||||
}
|
||||
|
||||
// Converts a string into a go-ethereum Hash object used as test input.
|
||||
func hashFromString(str string) ethCommon.Hash {
|
||||
if (len(str) > 2) && (str[0] == '0') && (str[1] == 'x') {
|
||||
str = str[2:]
|
||||
}
|
||||
|
||||
return ethCommon.HexToHash(str)
|
||||
}
|
||||
|
||||
// Note this method assumes 18 decimals for the amount.
|
||||
func buildMockTransferPayloadBytes(
|
||||
t uint8,
|
||||
tokenChainID vaa.ChainID,
|
||||
tokenAddrStr string,
|
||||
toChainID vaa.ChainID,
|
||||
toAddrStr string,
|
||||
amtFloat float64,
|
||||
) []byte {
|
||||
bytes := make([]byte, 101)
|
||||
bytes[0] = t
|
||||
|
||||
amtBigFloat := big.NewFloat(amtFloat)
|
||||
amtBigFloat = amtBigFloat.Mul(amtBigFloat, big.NewFloat(100000000))
|
||||
amount, _ := amtBigFloat.Int(nil)
|
||||
amtBytes := amount.Bytes()
|
||||
if len(amtBytes) > 32 {
|
||||
panic("amount will not fit in 32 bytes!")
|
||||
}
|
||||
copy(bytes[33-len(amtBytes):33], amtBytes)
|
||||
|
||||
tokenAddr, _ := vaa.StringToAddress(tokenAddrStr)
|
||||
copy(bytes[33:65], tokenAddr.Bytes())
|
||||
binary.BigEndian.PutUint16(bytes[65:67], uint16(tokenChainID))
|
||||
toAddr, _ := vaa.StringToAddress(toAddrStr)
|
||||
copy(bytes[67:99], toAddr.Bytes())
|
||||
binary.BigEndian.PutUint16(bytes[99:101], uint16(toChainID))
|
||||
return bytes
|
||||
}
|
||||
|
||||
func TestVaaFromUninterestingEmitter(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
acctChan := make(chan *common.MessagePublication, 10)
|
||||
acct := newAccountingForTest(t, ctx, enforceAccounting, acctChan)
|
||||
require.NotNil(t, acct)
|
||||
|
||||
emitterAddr, _ := vaa.StringToAddress("0x00")
|
||||
var payload = []byte{1, 97, 97, 97, 97, 97}
|
||||
|
||||
msg := common.MessagePublication{
|
||||
TxHash: hashFromString("0x06f541f5ecfc43407c31587aa6ac3a689e8960f36dc23c332db5510dfc6a4063"),
|
||||
Timestamp: time.Unix(int64(1654543099), 0),
|
||||
Nonce: uint32(1),
|
||||
Sequence: uint64(1),
|
||||
EmitterChain: vaa.ChainIDSolana,
|
||||
EmitterAddress: emitterAddr,
|
||||
ConsistencyLevel: uint8(32),
|
||||
Payload: payload,
|
||||
}
|
||||
|
||||
shouldPublish, err := acct.SubmitObservation(&msg)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, true, shouldPublish)
|
||||
assert.Equal(t, 0, len(acct.pendingTransfers))
|
||||
}
|
||||
|
||||
func TestVaaForUninterestingPayloadType(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
acctChan := make(chan *common.MessagePublication, 10)
|
||||
acct := newAccountingForTest(t, ctx, enforceAccounting, acctChan)
|
||||
require.NotNil(t, acct)
|
||||
|
||||
emitterAddr, _ := vaa.StringToAddress("0x0290fb167208af455bb137780163b7b7a9a10c16")
|
||||
var payload = []byte{2, 97, 97, 97, 97, 97}
|
||||
|
||||
msg := common.MessagePublication{
|
||||
TxHash: hashFromString("0x06f541f5ecfc43407c31587aa6ac3a689e8960f36dc23c332db5510dfc6a4063"),
|
||||
Timestamp: time.Unix(int64(1654543099), 0),
|
||||
Nonce: uint32(1),
|
||||
Sequence: uint64(1),
|
||||
EmitterChain: vaa.ChainIDEthereum,
|
||||
EmitterAddress: emitterAddr,
|
||||
ConsistencyLevel: uint8(32),
|
||||
Payload: payload,
|
||||
}
|
||||
|
||||
shouldPublish, err := acct.SubmitObservation(&msg)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, true, shouldPublish)
|
||||
assert.Equal(t, 0, len(acct.pendingTransfers))
|
||||
}
|
||||
|
||||
func TestInterestingTransferShouldNotBeBlockedWhenNotEnforcingAccounting(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
acctChan := make(chan *common.MessagePublication, 10)
|
||||
acct := newAccountingForTest(t, ctx, dontEnforceAccounting, acctChan)
|
||||
require.NotNil(t, acct)
|
||||
|
||||
emitterAddr, _ := vaa.StringToAddress("0000000000000000000000000290fb167208af455bb137780163b7b7a9a10c16")
|
||||
|
||||
payloadBytes := buildMockTransferPayloadBytes(1,
|
||||
vaa.ChainIDEthereum,
|
||||
"0x707f9118e33a9b8998bea41dd0d46f38bb963fc8",
|
||||
vaa.ChainIDPolygon,
|
||||
"0x707f9118e33a9b8998bea41dd0d46f38bb963fc8",
|
||||
1.25,
|
||||
)
|
||||
|
||||
msg := common.MessagePublication{
|
||||
TxHash: hashFromString("0x06f541f5ecfc43407c31587aa6ac3a689e8960f36dc23c332db5510dfc6a4063"),
|
||||
Timestamp: time.Unix(int64(1654543099), 0),
|
||||
Nonce: uint32(1),
|
||||
Sequence: uint64(1),
|
||||
EmitterChain: vaa.ChainIDEthereum,
|
||||
EmitterAddress: emitterAddr,
|
||||
ConsistencyLevel: uint8(32),
|
||||
Payload: payloadBytes,
|
||||
}
|
||||
|
||||
shouldPublish, err := acct.SubmitObservation(&msg)
|
||||
require.NoError(t, err)
|
||||
|
||||
// The transfer should not be blocked, but it should be in the pending map.
|
||||
assert.Equal(t, true, shouldPublish)
|
||||
pe, exists := acct.pendingTransfers[msg.MessageIDString()]
|
||||
require.Equal(t, true, exists)
|
||||
require.NotNil(t, pe)
|
||||
|
||||
// PublishTransfer should not publish to the channel but it should remove it from the map.
|
||||
acct.publishTransfer(pe)
|
||||
assert.Equal(t, 0, len(acct.msgChan))
|
||||
assert.Equal(t, 0, len(acct.pendingTransfers))
|
||||
}
|
||||
|
||||
func TestInterestingTransferShouldBeBlockedWhenEnforcingAccounting(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
acctChan := make(chan *common.MessagePublication, 10)
|
||||
acct := newAccountingForTest(t, ctx, enforceAccounting, acctChan)
|
||||
require.NotNil(t, acct)
|
||||
|
||||
emitterAddr, _ := vaa.StringToAddress("0000000000000000000000000290fb167208af455bb137780163b7b7a9a10c16")
|
||||
|
||||
payloadBytes := buildMockTransferPayloadBytes(1,
|
||||
vaa.ChainIDEthereum,
|
||||
"0x707f9118e33a9b8998bea41dd0d46f38bb963fc8",
|
||||
vaa.ChainIDPolygon,
|
||||
"0x707f9118e33a9b8998bea41dd0d46f38bb963fc8",
|
||||
1.25,
|
||||
)
|
||||
|
||||
msg := common.MessagePublication{
|
||||
TxHash: hashFromString("0x06f541f5ecfc43407c31587aa6ac3a689e8960f36dc23c332db5510dfc6a4063"),
|
||||
Timestamp: time.Unix(int64(1654543099), 0),
|
||||
Nonce: uint32(1),
|
||||
Sequence: uint64(1),
|
||||
EmitterChain: vaa.ChainIDEthereum,
|
||||
EmitterAddress: emitterAddr,
|
||||
ConsistencyLevel: uint8(32),
|
||||
Payload: payloadBytes,
|
||||
}
|
||||
|
||||
shouldPublish, err := acct.SubmitObservation(&msg)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, false, shouldPublish)
|
||||
assert.Equal(t, 1, len(acct.pendingTransfers))
|
||||
assert.Equal(t, 0, len(acct.msgChan))
|
||||
|
||||
// The same message a second time should still be blocked, but the pending map should not change.
|
||||
msg2 := msg
|
||||
shouldPublish, err = acct.SubmitObservation(&msg2)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, false, shouldPublish)
|
||||
assert.Equal(t, 0, len(acct.msgChan))
|
||||
pe, exists := acct.pendingTransfers[msg.MessageIDString()]
|
||||
require.Equal(t, true, exists)
|
||||
require.NotNil(t, pe)
|
||||
|
||||
// PublishTransfer should publish to the channel and remove it from the map.
|
||||
acct.publishTransfer(pe)
|
||||
assert.Equal(t, 1, len(acct.msgChan))
|
||||
assert.Equal(t, 0, len(acct.pendingTransfers))
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
package accounting
|
||||
|
||||
import (
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
var (
|
||||
transfersOutstanding = promauto.NewGauge(
|
||||
prometheus.GaugeOpts{
|
||||
Name: "wormhole_accounting_transfer_vaas_outstanding",
|
||||
Help: "Current number of accounting transfers vaas in the pending state",
|
||||
})
|
||||
transfersSubmitted = promauto.NewCounter(
|
||||
prometheus.CounterOpts{
|
||||
Name: "wormhole_accounting_transfer_vaas_submitted",
|
||||
Help: "Total number of accounting transfer vaas submitted",
|
||||
})
|
||||
transfersApproved = promauto.NewCounter(
|
||||
prometheus.CounterOpts{
|
||||
Name: "wormhole_accounting_transfer_vaas_submitted_and_approved",
|
||||
Help: "Total number of accounting transfer vaas that were submitted and approved",
|
||||
})
|
||||
eventsReceived = promauto.NewCounter(
|
||||
prometheus.CounterOpts{
|
||||
Name: "wormhole_accounting_events_received",
|
||||
Help: "Total number of accounting events received from the smart contract",
|
||||
})
|
||||
submitFailures = promauto.NewCounter(
|
||||
prometheus.CounterOpts{
|
||||
Name: "wormhole_accounting_submit_failures",
|
||||
Help: "Total number of accounting transfer vaas submit failures",
|
||||
})
|
||||
balanceErrors = promauto.NewCounter(
|
||||
prometheus.CounterOpts{
|
||||
Name: "wormhole_accounting_total_balance_errors",
|
||||
Help: "Total number of balance errors detected by accounting",
|
||||
})
|
||||
digestMismatches = promauto.NewCounter(
|
||||
prometheus.CounterOpts{
|
||||
Name: "wormhole_accounting_total_digest_mismatches",
|
||||
Help: "Total number of digest mismatches on accounting",
|
||||
})
|
||||
connectionErrors = promauto.NewCounter(
|
||||
prometheus.CounterOpts{
|
||||
Name: "wormhole_accounting_connection_errors_total",
|
||||
Help: "Total number of connection errors on accounting",
|
||||
})
|
||||
)
|
|
@ -0,0 +1,273 @@
|
|||
package accounting
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/certusone/wormhole/node/pkg/common"
|
||||
"github.com/certusone/wormhole/node/pkg/wormconn"
|
||||
"github.com/wormhole-foundation/wormhole/sdk/vaa"
|
||||
|
||||
ethCrypto "github.com/ethereum/go-ethereum/crypto"
|
||||
|
||||
wasmdtypes "github.com/CosmWasm/wasmd/x/wasm/types"
|
||||
sdktypes "github.com/cosmos/cosmos-sdk/types"
|
||||
sdktx "github.com/cosmos/cosmos-sdk/types/tx"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func (acct *Accounting) worker(ctx context.Context) error {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case msg := <-acct.subChan:
|
||||
gs := acct.gst.Get()
|
||||
if gs == nil {
|
||||
acct.logger.Error("acct: unable to send observation request: failed to look up guardian set", zap.String("msgID", msg.MessageIDString()))
|
||||
continue
|
||||
}
|
||||
|
||||
guardianIndex, found := gs.KeyIndex(acct.guardianAddr)
|
||||
if !found {
|
||||
acct.logger.Error("acct: unable to send observation request: failed to look up guardian index",
|
||||
zap.String("msgID", msg.MessageIDString()), zap.Stringer("guardianAddr", acct.guardianAddr))
|
||||
continue
|
||||
}
|
||||
|
||||
acct.submitObservationToContract(msg, gs.Index, uint32(guardianIndex))
|
||||
transfersSubmitted.Inc()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type (
|
||||
SubmitObservationsMsg struct {
|
||||
Params SubmitObservationsParams `json:"submit_observations"`
|
||||
}
|
||||
|
||||
SubmitObservationsParams struct {
|
||||
// A serialized `Vec<Observation>`. Multiple observations can be submitted together to reduce transaction overhead.
|
||||
Observations []byte `json:"observations"`
|
||||
|
||||
// The index of the guardian set used to sign the observations.
|
||||
GuardianSetIndex uint32 `json:"guardian_set_index"`
|
||||
|
||||
// A signature for `observations`.
|
||||
Signature SignatureType `json:"signature"`
|
||||
}
|
||||
|
||||
SignatureType struct {
|
||||
Index uint32 `json:"index"`
|
||||
Signature SignatureBytes `json:"signature"`
|
||||
}
|
||||
|
||||
SignatureBytes []uint8
|
||||
|
||||
Observation struct {
|
||||
// The hash of the transaction on the emitter chain in which the transfer was performed.
|
||||
TxHash []byte `json:"tx_hash"`
|
||||
|
||||
// Seconds since UNIX epoch.
|
||||
Timestamp uint32 `json:"timestamp"`
|
||||
|
||||
// The nonce for the transfer.
|
||||
Nonce uint32 `json:"nonce"`
|
||||
|
||||
// The source chain from which this observation was created.
|
||||
EmitterChain uint16 `json:"emitter_chain"`
|
||||
|
||||
// The address on the source chain that emitted this message.
|
||||
EmitterAddress [32]byte `json:"emitter_address"`
|
||||
|
||||
// The sequence number of this observation.
|
||||
Sequence uint64 `json:"sequence"`
|
||||
|
||||
// The consistency level requested by the emitter.
|
||||
ConsistencyLevel uint8 `json:"consistency_level"`
|
||||
|
||||
// The serialized tokenbridge payload.
|
||||
Payload []byte `json:"payload"`
|
||||
}
|
||||
)
|
||||
|
||||
func (sb SignatureBytes) MarshalJSON() ([]byte, error) {
|
||||
var result string
|
||||
if sb == nil {
|
||||
result = "null"
|
||||
} else {
|
||||
result = strings.Join(strings.Fields(fmt.Sprintf("%d", sb)), ",")
|
||||
}
|
||||
return []byte(result), nil
|
||||
}
|
||||
|
||||
// submitObservationToContract makes a call to the smart contract to submit an observation request.
|
||||
// It should be called from a go routine because it can block.
|
||||
func (acct *Accounting) submitObservationToContract(msg *common.MessagePublication, gsIndex uint32, guardianIndex uint32) {
|
||||
msgId := msg.MessageIDString()
|
||||
acct.logger.Debug("acct: in submitObservationToContract", zap.String("msgID", msgId))
|
||||
txResp, err := SubmitObservationToContract(acct.ctx, acct.logger, acct.gk, gsIndex, guardianIndex, acct.wormchainConn, acct.contract, msg)
|
||||
if err != nil {
|
||||
acct.logger.Error("acct: failed to submit observation request", zap.String("msgId", msgId), zap.Error(err))
|
||||
submitFailures.Inc()
|
||||
return
|
||||
}
|
||||
|
||||
alreadyCommitted, err := CheckSubmitObservationResult(txResp)
|
||||
if err != nil {
|
||||
submitFailures.Inc()
|
||||
if strings.Contains(err.Error(), "insufficient balance") {
|
||||
balanceErrors.Inc()
|
||||
acct.logger.Error("acct: insufficient balance error detected, dropping transfer", zap.String("msgId", msgId), zap.Error(err))
|
||||
acct.pendingTransfersLock.Lock()
|
||||
defer acct.pendingTransfersLock.Unlock()
|
||||
acct.deletePendingTransfer(msgId)
|
||||
} else {
|
||||
acct.logger.Error("acct: failed to submit observation request", zap.String("msgId", msgId), zap.Error(err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if alreadyCommitted {
|
||||
acct.pendingTransfersLock.Lock()
|
||||
defer acct.pendingTransfersLock.Unlock()
|
||||
pe, exists := acct.pendingTransfers[msgId]
|
||||
if exists {
|
||||
acct.logger.Info("acct: transfer has already been committed, publishing it", zap.String("msgId", msgId))
|
||||
acct.publishTransfer(pe)
|
||||
transfersApproved.Inc()
|
||||
} else {
|
||||
acct.logger.Debug("acct: transfer has already been committed, and it is no longer in our map", zap.String("msgId", msgId))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SubmitObservationToContract is a free function to make a call to the smart contract to submit an observation request.
|
||||
func SubmitObservationToContract(
|
||||
ctx context.Context,
|
||||
logger *zap.Logger,
|
||||
gk *ecdsa.PrivateKey,
|
||||
gsIndex uint32,
|
||||
guardianIndex uint32,
|
||||
wormchainConn *wormconn.ClientConn,
|
||||
contract string,
|
||||
msg *common.MessagePublication,
|
||||
) (*sdktx.BroadcastTxResponse, error) {
|
||||
obs := []Observation{
|
||||
Observation{
|
||||
TxHash: msg.TxHash.Bytes(),
|
||||
Timestamp: uint32(msg.Timestamp.Unix()),
|
||||
Nonce: msg.Nonce,
|
||||
EmitterChain: uint16(msg.EmitterChain),
|
||||
EmitterAddress: msg.EmitterAddress,
|
||||
Sequence: msg.Sequence,
|
||||
ConsistencyLevel: msg.ConsistencyLevel,
|
||||
Payload: msg.Payload,
|
||||
},
|
||||
}
|
||||
|
||||
bytes, err := json.Marshal(obs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("acct: failed to marshal accounting observation request: %w", err)
|
||||
}
|
||||
|
||||
digest := vaa.SigningMsg(bytes)
|
||||
|
||||
sigBytes, err := ethCrypto.Sign(digest.Bytes(), gk)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("acct: failed to sign accounting Observation request: %w", err)
|
||||
}
|
||||
|
||||
sig := SignatureType{Index: guardianIndex, Signature: sigBytes}
|
||||
|
||||
msgData := SubmitObservationsMsg{
|
||||
Params: SubmitObservationsParams{
|
||||
Observations: bytes,
|
||||
GuardianSetIndex: gsIndex,
|
||||
Signature: sig,
|
||||
},
|
||||
}
|
||||
|
||||
msgBytes, err := json.Marshal(msgData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("acct: failed to marshal accounting observation request: %w", err)
|
||||
}
|
||||
|
||||
subMsg := wasmdtypes.MsgExecuteContract{
|
||||
Sender: wormchainConn.SenderAddress(),
|
||||
Contract: contract,
|
||||
Msg: msgBytes,
|
||||
Funds: sdktypes.Coins{},
|
||||
}
|
||||
|
||||
logger.Debug("acct: in SubmitObservationToContract, sending broadcast",
|
||||
zap.String("txHash", msg.TxHash.String()), zap.String("encTxHash", hex.EncodeToString(obs[0].TxHash[:])),
|
||||
zap.Stringer("timeStamp", msg.Timestamp), zap.Uint32("encTimestamp", obs[0].Timestamp),
|
||||
zap.Uint32("nonce", msg.Nonce), zap.Uint32("encNonce", obs[0].Nonce),
|
||||
zap.Stringer("emitterChain", msg.EmitterChain), zap.Uint16("encEmitterChain", obs[0].EmitterChain),
|
||||
zap.Stringer("emitterAddress", msg.EmitterAddress), zap.String("encEmitterAddress", hex.EncodeToString(obs[0].EmitterAddress[:])),
|
||||
zap.Uint64("squence", msg.Sequence), zap.Uint64("encSequence", obs[0].Sequence),
|
||||
zap.Uint8("consistencyLevel", msg.ConsistencyLevel), zap.Uint8("encConsistencyLevel", obs[0].ConsistencyLevel),
|
||||
zap.String("payload", hex.EncodeToString(msg.Payload)), zap.String("encPayload", hex.EncodeToString(obs[0].Payload)),
|
||||
zap.String("observations", string(bytes)),
|
||||
zap.Uint32("gsIndex", gsIndex), zap.Uint32("guardianIndex", guardianIndex),
|
||||
)
|
||||
|
||||
txResp, err := wormchainConn.SignAndBroadcastTx(ctx, &subMsg)
|
||||
if err != nil {
|
||||
logger.Error("acct: SubmitObservationToContract failed to send broadcast", zap.Error(err))
|
||||
} else {
|
||||
if txResp.TxResponse == nil {
|
||||
return txResp, fmt.Errorf("txResp.TxResponse is nil")
|
||||
}
|
||||
if strings.Contains(txResp.TxResponse.RawLog, "out of gas") {
|
||||
return txResp, fmt.Errorf("out of gas: %s", txResp.TxResponse.RawLog)
|
||||
}
|
||||
|
||||
out, err := wormchainConn.BroadcastTxResponseToString(txResp)
|
||||
if err != nil {
|
||||
logger.Error("acct: SubmitObservationToContract failed to parse broadcast response", zap.Error(err))
|
||||
} else {
|
||||
logger.Debug("acct: in SubmitObservationToContract, done sending broadcast", zap.String("resp", out))
|
||||
}
|
||||
}
|
||||
return txResp, err
|
||||
}
|
||||
|
||||
// CheckSubmitObservationResult() is a free function that returns true if the observation has already been committed
|
||||
// or false if we need to wait for the commit. An error is returned when appropriate.
|
||||
func CheckSubmitObservationResult(txResp *sdktx.BroadcastTxResponse) (bool, error) {
|
||||
if txResp == nil {
|
||||
return false, fmt.Errorf("txResp is nil")
|
||||
}
|
||||
if txResp.TxResponse == nil {
|
||||
return false, fmt.Errorf("txResp does not contain a TxResponse")
|
||||
}
|
||||
if txResp.TxResponse.RawLog == "" {
|
||||
return false, fmt.Errorf("RawLog is not set")
|
||||
}
|
||||
if strings.Contains(txResp.TxResponse.RawLog, "execute wasm contract failed") {
|
||||
if strings.Contains(txResp.TxResponse.RawLog, "already committed") {
|
||||
return true, nil
|
||||
|
||||
}
|
||||
|
||||
// TODO Need to test this, requires multiple guardians.
|
||||
if strings.Contains(txResp.TxResponse.RawLog, "duplicate signature") {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
return false, fmt.Errorf(txResp.TxResponse.RawLog)
|
||||
}
|
||||
|
||||
if strings.Contains(txResp.TxResponse.RawLog, "failed to execute message") {
|
||||
return false, fmt.Errorf(txResp.TxResponse.RawLog)
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
|
@ -0,0 +1,198 @@
|
|||
package accounting
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"github.com/certusone/wormhole/node/pkg/common"
|
||||
"github.com/wormhole-foundation/wormhole/sdk/vaa"
|
||||
|
||||
ethCommon "github.com/ethereum/go-ethereum/common"
|
||||
|
||||
tmAbci "github.com/tendermint/tendermint/abci/types"
|
||||
tmHttp "github.com/tendermint/tendermint/rpc/client/http"
|
||||
tmCoreTypes "github.com/tendermint/tendermint/rpc/core/types"
|
||||
tmTypes "github.com/tendermint/tendermint/types"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// watcher reads transaction events from the smart contract and publishes them.
|
||||
func (acct *Accounting) watcher(ctx context.Context) error {
|
||||
errC := make(chan error)
|
||||
|
||||
acct.logger.Info("acctwatch: creating watcher", zap.String("url", acct.wsUrl), zap.String("contract", acct.contract))
|
||||
tmConn, err := tmHttp.New(acct.wsUrl, "/websocket")
|
||||
if err != nil {
|
||||
connectionErrors.Inc()
|
||||
return fmt.Errorf("failed to establish tendermint connection: %w", err)
|
||||
}
|
||||
|
||||
if err := tmConn.Start(); err != nil {
|
||||
connectionErrors.Inc()
|
||||
return fmt.Errorf("failed to start tendermint connection: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := tmConn.Stop(); err != nil {
|
||||
connectionErrors.Inc()
|
||||
acct.logger.Error("acctwatch: failed to stop tendermint connection", zap.Error(err))
|
||||
}
|
||||
}()
|
||||
|
||||
query := fmt.Sprintf("execute._contract_address='%s'", acct.contract)
|
||||
events, err := tmConn.Subscribe(
|
||||
ctx,
|
||||
"guardiand",
|
||||
query,
|
||||
64, // channel capacity
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to subscribe to accounting events: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
if err := tmConn.UnsubscribeAll(ctx, "guardiand"); err != nil {
|
||||
acct.logger.Error("acctwatch: failed to unsubscribe from events", zap.Error(err))
|
||||
}
|
||||
}()
|
||||
|
||||
go acct.handleEvents(ctx, events, errC)
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case err := <-errC:
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// handleEvents handles events from the tendermint client library.
|
||||
func (acct *Accounting) handleEvents(ctx context.Context, evts <-chan tmCoreTypes.ResultEvent, errC chan error) {
|
||||
defer close(errC)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case e := <-evts:
|
||||
tx, ok := e.Data.(tmTypes.EventDataTx)
|
||||
if !ok {
|
||||
acct.logger.Error("acctwatcher: unknown data from event subscription", zap.Stringer("e.Data", reflect.TypeOf(e.Data)), zap.Any("event", e))
|
||||
continue
|
||||
}
|
||||
|
||||
for _, event := range tx.Result.Events {
|
||||
if event.Type == "wasm-Transfer" {
|
||||
xfer, err := parseWasmTransfer(acct.logger, event, acct.contract)
|
||||
if err != nil {
|
||||
acct.logger.Error("acctwatcher: failed to parse wasm event", zap.Error(err), zap.Stringer("e.Data", reflect.TypeOf(e.Data)), zap.Any("event", event))
|
||||
continue
|
||||
}
|
||||
|
||||
eventsReceived.Inc()
|
||||
acct.processPendingTransfer(xfer)
|
||||
} else {
|
||||
acct.logger.Debug("acctwatcher: ignoring non-transfer event", zap.String("eventType", event.Type))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WasmTransfer represents a transfer event from the smart contract.
|
||||
type WasmTransfer struct {
|
||||
TxHashBytes []byte `json:"tx_hash"`
|
||||
Timestamp uint32 `json:"timestamp"`
|
||||
Nonce uint32 `json:"nonce"`
|
||||
EmitterChain uint16 `json:"emitter_chain"`
|
||||
EmitterAddress vaa.Address `json:"emitter_address"`
|
||||
Sequence uint64 `json:"sequence"`
|
||||
ConsistencyLevel uint8 `json:"consistency_level"`
|
||||
Payload []byte `json:"payload"`
|
||||
}
|
||||
|
||||
// parseWasmTransfer parses transfer events from the smart contract. All other event types are ignored.
|
||||
func parseWasmTransfer(logger *zap.Logger, event tmAbci.Event, contractAddress string) (*WasmTransfer, error) {
|
||||
if event.Type != "wasm-Transfer" {
|
||||
return nil, fmt.Errorf("not a WasmTransfer event: %s", event.Type)
|
||||
}
|
||||
|
||||
attrs := make(map[string]json.RawMessage)
|
||||
for _, attr := range event.Attributes {
|
||||
if string(attr.Key) == "_contract_address" {
|
||||
if string(attr.Value) != contractAddress {
|
||||
return nil, fmt.Errorf("WasmTransfer event from unexpected contract: %s", string(attr.Value))
|
||||
}
|
||||
} else {
|
||||
logger.Debug("acctwatcher: attribute", zap.String("key", string(attr.Key)), zap.String("value", string(attr.Value)))
|
||||
attrs[string(attr.Key)] = attr.Value
|
||||
}
|
||||
}
|
||||
|
||||
attrBytes, err := json.Marshal(attrs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal event attributes: %w", err)
|
||||
}
|
||||
|
||||
evt := new(WasmTransfer)
|
||||
if err := json.Unmarshal(attrBytes, evt); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal WasmTransfer event: %w", err)
|
||||
}
|
||||
|
||||
return evt, nil
|
||||
}
|
||||
|
||||
// processPendingTransfer takes a WasmTransfer event, determines if we are expecting it, and if so, publishes it.
|
||||
func (acct *Accounting) processPendingTransfer(xfer *WasmTransfer) {
|
||||
acct.logger.Info("acctwatch: transfer event detected",
|
||||
zap.String("tx_hash", hex.EncodeToString(xfer.TxHashBytes)),
|
||||
zap.Uint32("timestamp", xfer.Timestamp),
|
||||
zap.Uint32("nonce", xfer.Nonce),
|
||||
zap.Stringer("emitter_chain", vaa.ChainID(xfer.EmitterChain)),
|
||||
zap.Stringer("emitter_address", xfer.EmitterAddress),
|
||||
zap.Uint64("sequence", xfer.Sequence),
|
||||
zap.Uint8("consistency_level", xfer.ConsistencyLevel),
|
||||
zap.String("payload", hex.EncodeToString(xfer.Payload)),
|
||||
)
|
||||
|
||||
msg := &common.MessagePublication{
|
||||
TxHash: ethCommon.BytesToHash(xfer.TxHashBytes),
|
||||
Timestamp: time.Unix(int64(xfer.Timestamp), 0),
|
||||
Nonce: xfer.Nonce,
|
||||
Sequence: xfer.Sequence,
|
||||
EmitterChain: vaa.ChainID(xfer.EmitterChain),
|
||||
EmitterAddress: xfer.EmitterAddress,
|
||||
Payload: xfer.Payload,
|
||||
ConsistencyLevel: xfer.ConsistencyLevel,
|
||||
}
|
||||
|
||||
msgId := msg.MessageIDString()
|
||||
|
||||
acct.pendingTransfersLock.Lock()
|
||||
defer acct.pendingTransfersLock.Unlock()
|
||||
|
||||
pe, exists := acct.pendingTransfers[msgId]
|
||||
if exists {
|
||||
digest := msg.CreateDigest()
|
||||
if pe.digest != digest {
|
||||
digestMismatches.Inc()
|
||||
acct.logger.Error("acctwatch: digest mismatch, dropping transfer",
|
||||
zap.String("msgID", msgId),
|
||||
zap.String("oldDigest", pe.digest),
|
||||
zap.String("newDigest", digest),
|
||||
)
|
||||
|
||||
acct.deletePendingTransfer(msgId)
|
||||
return
|
||||
}
|
||||
acct.logger.Info("acctwatch: pending transfer has been approved", zap.String("msgId", msgId))
|
||||
acct.publishTransfer(pe)
|
||||
transfersApproved.Inc()
|
||||
} else {
|
||||
// TODO: We could issue a reobservation request here since it looks like other guardians have seen this transfer but we haven't.
|
||||
acct.logger.Info("acctwatch: unknown transfer has been approved, ignoring it", zap.String("msgId", msgId))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
package accounting
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/wormhole-foundation/wormhole/sdk/vaa"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
tmAbci "github.com/tendermint/tendermint/abci/types"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func TestParseWasmTransferFromTestTool(t *testing.T) {
|
||||
logger := zap.NewNop()
|
||||
|
||||
eventJson := []byte("{\"type\":\"wasm-Transfer\",\"attributes\":[{\"key\":\"X2NvbnRyYWN0X2FkZHJlc3M=\",\"value\":\"d29ybWhvbGUxNDY2bmYzenV4cHlhOHE5ZW14dWtkN3ZmdGFmNmg0cHNyMGEwN3NybDV6dzc0emg4NHlqcTRseWptaA==\",\"index\":true},{\"key\":\"dHhfaGFzaA==\",\"value\":\"Imd1b2xOc1hSWnhnd3kwa1NENVJIbmpTMVJaYW8zVGFmdkNabVpucDJYMHM9Ig==\",\"index\":true},{\"key\":\"dGltZXN0YW1w\",\"value\":\"MTY3MjkzMjk5OA==\",\"index\":true},{\"key\":\"bm9uY2U=\",\"value\":\"MA==\",\"index\":true},{\"key\":\"ZW1pdHRlcl9jaGFpbg==\",\"value\":\"Mg==\",\"index\":true},{\"key\":\"ZW1pdHRlcl9hZGRyZXNz\",\"value\":\"IjAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAyOTBmYjE2NzIwOGFmNDU1YmIxMzc3ODAxNjNiN2I3YTlhMTBjMTYi\",\"index\":true},{\"key\":\"c2VxdWVuY2U=\",\"value\":\"MTY3MjkzMjk5OA==\",\"index\":true},{\"key\":\"Y29uc2lzdGVuY3lfbGV2ZWw=\",\"value\":\"MTU=\",\"index\":true},{\"key\":\"dGVzdF9maWVsZA==\",\"value\":\"MTU=\",\"index\":true},{\"key\":\"cGF5bG9hZA==\",\"value\":\"IkFRQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUEzZ3RyT25aQUFBQUFBQUFBQUFBQUFBQUFBQUxZdm12d3VxZE9DcEJ3Rm1lY3JwR1E2QTNRb0FBZ0FBQUFBQUFBQUFBQUFBQU1FSUlKZy9NMFZzNTc2em9FYjFxRCtqVHdKOURDQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUE9PSI=\",\"index\":true}]}")
|
||||
event := tmAbci.Event{}
|
||||
err := json.Unmarshal(eventJson, &event)
|
||||
require.NoError(t, err)
|
||||
|
||||
xfer, err := parseWasmTransfer(logger, event, "wormhole1466nf3zuxpya8q9emxukd7vftaf6h4psr0a07srl5zw74zh84yjq4lyjmh")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, xfer)
|
||||
|
||||
expectedTxHash, err := vaa.StringToHash("82ea2536c5d1671830cb49120f94479e34b54596a8dd369fbc2666667a765f4b")
|
||||
require.NoError(t, err)
|
||||
|
||||
expectedEmitterAddress, err := vaa.StringToAddress("0000000000000000000000000290fb167208af455bb137780163b7b7a9a10c16")
|
||||
require.NoError(t, err)
|
||||
|
||||
expectedPayload, err := hex.DecodeString("010000000000000000000000000000000000000000000000000de0b6b3a76400000000000000000000000000002d8be6bf0baa74e0a907016679cae9190e80dd0a0002000000000000000000000000c10820983f33456ce7beb3a046f5a83fa34f027d0c200000000000000000000000000000000000000000000000000000000000000000")
|
||||
require.NoError(t, err)
|
||||
|
||||
expectedResult := WasmTransfer{
|
||||
TxHashBytes: expectedTxHash.Bytes(),
|
||||
Timestamp: 1672932998,
|
||||
Nonce: 0,
|
||||
EmitterChain: uint16(vaa.ChainIDEthereum),
|
||||
EmitterAddress: expectedEmitterAddress,
|
||||
Sequence: 1672932998,
|
||||
ConsistencyLevel: 15,
|
||||
Payload: expectedPayload,
|
||||
}
|
||||
assert.Equal(t, expectedResult, *xfer)
|
||||
}
|
||||
|
||||
func TestParseWasmTransferFromPortalBridge(t *testing.T) {
|
||||
logger := zap.NewNop()
|
||||
|
||||
eventJson := []byte("{\"type\":\"wasm-Transfer\",\"attributes\":[{\"key\":\"X2NvbnRyYWN0X2FkZHJlc3M=\",\"value\":\"d29ybWhvbGUxNDY2bmYzenV4cHlhOHE5ZW14dWtkN3ZmdGFmNmg0cHNyMGEwN3NybDV6dzc0emg4NHlqcTRseWptaA==\",\"index\":true},{\"key\":\"dHhfaGFzaA==\",\"value\":\"IlovM0x1bklSK0FaWjdRdllqS0dHSDBNZU94M1pIZlR1SHZ6TDAxdm9TcjQ9Ig==\",\"index\":true},{\"key\":\"dGltZXN0YW1w\",\"value\":\"OTUwNw==\",\"index\":true},{\"key\":\"bm9uY2U=\",\"value\":\"NTU0MzAzNzQ0\",\"index\":true},{\"key\":\"ZW1pdHRlcl9jaGFpbg==\",\"value\":\"Mg==\",\"index\":true},{\"key\":\"ZW1pdHRlcl9hZGRyZXNz\",\"value\":\"IjAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAyOTBmYjE2NzIwOGFmNDU1YmIxMzc3ODAxNjNiN2I3YTlhMTBjMTYi\",\"index\":true},{\"key\":\"c2VxdWVuY2U=\",\"value\":\"MQ==\",\"index\":true},{\"key\":\"Y29uc2lzdGVuY3lfbGV2ZWw=\",\"value\":\"MQ==\",\"index\":true},{\"key\":\"cGF5bG9hZA==\",\"value\":\"IkFRQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBSlVDK1FBQUFBQUFBQUFBQUFBQUFBQTNiWlA1R3FSMUc3aWxDQlRuOEpmMEh4ZjZqNEFBZ0FBQUFBQUFBQUFBQUFBQUpENHYycEhueklPclFkRUVhU3c1NVJPcU1uQkFBUUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUE9PSI=\",\"index\":true}]}")
|
||||
event := tmAbci.Event{}
|
||||
err := json.Unmarshal(eventJson, &event)
|
||||
require.NoError(t, err)
|
||||
|
||||
xfer, err := parseWasmTransfer(logger, event, "wormhole1466nf3zuxpya8q9emxukd7vftaf6h4psr0a07srl5zw74zh84yjq4lyjmh")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, xfer)
|
||||
|
||||
expectedTxHash, err := vaa.StringToHash("67fdcbba7211f80659ed0bd88ca1861f431e3b1dd91df4ee1efccbd35be84abe")
|
||||
require.NoError(t, err)
|
||||
|
||||
expectedEmitterAddress, err := vaa.StringToAddress("0000000000000000000000000290fb167208af455bb137780163b7b7a9a10c16")
|
||||
require.NoError(t, err)
|
||||
|
||||
expectedPayload, err := hex.DecodeString("0100000000000000000000000000000000000000000000000000000002540be400000000000000000000000000ddb64fe46a91d46ee29420539fc25fd07c5fea3e000200000000000000000000000090f8bf6a479f320ead074411a4b0e7944ea8c9c100040000000000000000000000000000000000000000000000000000000000000000")
|
||||
require.NoError(t, err)
|
||||
|
||||
expectedResult := WasmTransfer{
|
||||
TxHashBytes: expectedTxHash.Bytes(),
|
||||
Timestamp: 9507,
|
||||
Nonce: 554303744,
|
||||
EmitterChain: uint16(vaa.ChainIDEthereum),
|
||||
EmitterAddress: expectedEmitterAddress,
|
||||
Sequence: 1,
|
||||
ConsistencyLevel: 1,
|
||||
Payload: expectedPayload,
|
||||
}
|
||||
|
||||
assert.Equal(t, expectedResult, *xfer)
|
||||
}
|
|
@ -3,6 +3,8 @@ package common
|
|||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
|
@ -106,6 +108,33 @@ func UnmarshalMessagePublication(data []byte) (*MessagePublication, error) {
|
|||
return msg, nil
|
||||
}
|
||||
|
||||
// The standard json Marshal / Unmarshal of time.Time gets confused between local and UTC time.
|
||||
func (msg *MessagePublication) MarshalJSON() ([]byte, error) {
|
||||
type Alias MessagePublication
|
||||
return json.Marshal(&struct {
|
||||
Timestamp int64
|
||||
*Alias
|
||||
}{
|
||||
Timestamp: msg.Timestamp.Unix(),
|
||||
Alias: (*Alias)(msg),
|
||||
})
|
||||
}
|
||||
|
||||
func (msg *MessagePublication) UnmarshalJSON(data []byte) error {
|
||||
type Alias MessagePublication
|
||||
aux := &struct {
|
||||
Timestamp int64
|
||||
*Alias
|
||||
}{
|
||||
Alias: (*Alias)(msg),
|
||||
}
|
||||
if err := json.Unmarshal(data, &aux); err != nil {
|
||||
return err
|
||||
}
|
||||
msg.Timestamp = time.Unix(aux.Timestamp, 0)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (msg *MessagePublication) CreateVAA(gsIndex uint32) *vaa.VAA {
|
||||
return &vaa.VAA{
|
||||
Version: vaa.SupportedVAAVersion,
|
||||
|
@ -120,3 +149,9 @@ func (msg *MessagePublication) CreateVAA(gsIndex uint32) *vaa.VAA {
|
|||
ConsistencyLevel: msg.ConsistencyLevel,
|
||||
}
|
||||
}
|
||||
|
||||
func (msg *MessagePublication) CreateDigest() string {
|
||||
v := msg.CreateVAA(0) // The guardian set index is not part of the digest, so we can pass in zero.
|
||||
db := v.SigningMsg()
|
||||
return hex.EncodeToString(db.Bytes())
|
||||
}
|
||||
|
|
|
@ -74,6 +74,52 @@ func TestSerializeAndDeserializeOfMessagePublication(t *testing.T) {
|
|||
assert.Equal(t, payload1, payload2)
|
||||
}
|
||||
|
||||
func TestMarshalUnmarshalJSONOfMessagePublication(t *testing.T) {
|
||||
originAddress, err := vaa.StringToAddress("0xDDb64fE46a91D46ee29420539FC25FD07c5FEa3E") //nolint:gosec
|
||||
require.NoError(t, err)
|
||||
|
||||
targetAddress, err := vaa.StringToAddress("0x707f9118e33a9b8998bea41dd0d46f38bb963fc8")
|
||||
require.NoError(t, err)
|
||||
|
||||
tokenBridgeAddress, err := vaa.StringToAddress("0x707f9118e33a9b8998bea41dd0d46f38bb963fc8")
|
||||
require.NoError(t, err)
|
||||
|
||||
payload1 := &vaa.TransferPayloadHdr{
|
||||
Type: 0x01,
|
||||
Amount: big.NewInt(27000000000),
|
||||
OriginAddress: originAddress,
|
||||
OriginChain: vaa.ChainIDEthereum,
|
||||
TargetAddress: targetAddress,
|
||||
TargetChain: vaa.ChainIDPolygon,
|
||||
}
|
||||
|
||||
payloadBytes1 := encodePayloadBytes(payload1)
|
||||
|
||||
msg1 := &MessagePublication{
|
||||
TxHash: eth_common.HexToHash("0x06f541f5ecfc43407c31587aa6ac3a689e8960f36dc23c332db5510dfc6a4063"),
|
||||
Timestamp: time.Unix(int64(1654516425), 0),
|
||||
Nonce: 123456,
|
||||
Sequence: 789101112131415,
|
||||
EmitterChain: vaa.ChainIDEthereum,
|
||||
EmitterAddress: tokenBridgeAddress,
|
||||
Payload: payloadBytes1,
|
||||
ConsistencyLevel: 32,
|
||||
}
|
||||
|
||||
bytes, err := msg1.MarshalJSON()
|
||||
require.NoError(t, err)
|
||||
|
||||
var msg2 MessagePublication
|
||||
err = msg2.UnmarshalJSON(bytes)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, *msg1, msg2)
|
||||
|
||||
payload2, err := vaa.DecodeTransferPayloadHdr(msg2.Payload)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, *payload1, *payload2)
|
||||
}
|
||||
|
||||
func TestMessageIDString(t *testing.T) {
|
||||
addr, err := vaa.StringToAddress("0x0290fb167208af455bb137780163b7b7a9a10c16")
|
||||
require.NoError(t, err)
|
||||
|
|
|
@ -0,0 +1,111 @@
|
|||
package db
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/certusone/wormhole/node/pkg/common"
|
||||
"github.com/dgraph-io/badger/v3"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type AccountingDB interface {
|
||||
AcctStorePendingTransfer(msg *common.MessagePublication) error
|
||||
AcctDeletePendingTransfer(msgId string) error
|
||||
AcctGetData(logger *zap.Logger) ([]*common.MessagePublication, error)
|
||||
}
|
||||
|
||||
type MockAccountingDB struct {
|
||||
}
|
||||
|
||||
func (d *MockAccountingDB) AcctStorePendingTransfer(msg *common.MessagePublication) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *MockAccountingDB) AcctDeletePendingTransfer(msgId string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *MockAccountingDB) AcctGetData(logger *zap.Logger) ([]*common.MessagePublication, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
const acctPendingTransfer = "ACCT:PXFER:"
|
||||
const acctPendingTransferLen = len(acctPendingTransfer)
|
||||
|
||||
const acctMinMsgIdLen = len("1/0000000000000000000000000290fb167208af455bb137780163b7b7a9a10c16/0")
|
||||
|
||||
func acctPendingTransferMsgID(msgId string) []byte {
|
||||
return []byte(fmt.Sprintf("%v%v", acctPendingTransfer, msgId))
|
||||
}
|
||||
|
||||
func acctIsPendingTransfer(keyBytes []byte) bool {
|
||||
return (len(keyBytes) >= acctPendingTransferLen+acctMinMsgIdLen) && (string(keyBytes[0:acctPendingTransferLen]) == acctPendingTransfer)
|
||||
}
|
||||
|
||||
// This is called by the accounting module on start up to reload pending transfers.
|
||||
func (d *Database) AcctGetData(logger *zap.Logger) ([]*common.MessagePublication, error) {
|
||||
pendingTransfers := []*common.MessagePublication{}
|
||||
prefixBytes := []byte(acctPendingTransfer)
|
||||
err := d.db.View(func(txn *badger.Txn) error {
|
||||
opts := badger.DefaultIteratorOptions
|
||||
opts.PrefetchSize = 10
|
||||
it := txn.NewIterator(opts)
|
||||
defer it.Close()
|
||||
for it.Seek(prefixBytes); it.ValidForPrefix(prefixBytes); it.Next() {
|
||||
item := it.Item()
|
||||
key := item.Key()
|
||||
val, err := item.ValueCopy(nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if acctIsPendingTransfer(key) {
|
||||
var pt common.MessagePublication
|
||||
err := json.Unmarshal(val, &pt)
|
||||
if err != nil {
|
||||
logger.Error("acct: failed to unmarshal pending transfer for key", zap.String("key", string(key[:])), zap.Error(err))
|
||||
continue
|
||||
}
|
||||
|
||||
pendingTransfers = append(pendingTransfers, &pt)
|
||||
} else {
|
||||
return fmt.Errorf("unexpected accounting pending transfer key '%s'", string(key))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return pendingTransfers, err
|
||||
}
|
||||
|
||||
func (d *Database) AcctStorePendingTransfer(msg *common.MessagePublication) error {
|
||||
b, _ := json.Marshal(msg)
|
||||
|
||||
err := d.db.Update(func(txn *badger.Txn) error {
|
||||
if err := txn.Set(acctPendingTransferMsgID(msg.MessageIDString()), b); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to commit accounting pending transfer for tx %s: %w", msg.MessageIDString(), err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Database) AcctDeletePendingTransfer(msgId string) error {
|
||||
key := acctPendingTransferMsgID(msgId)
|
||||
if err := d.db.Update(func(txn *badger.Txn) error {
|
||||
err := txn.Delete(key)
|
||||
return err
|
||||
}); err != nil {
|
||||
return fmt.Errorf("failed to delete accounting pending transfer for tx %s: %w", msgId, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,195 @@
|
|||
package db
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/certusone/wormhole/node/pkg/common"
|
||||
"github.com/wormhole-foundation/wormhole/sdk/vaa"
|
||||
|
||||
"github.com/dgraph-io/badger/v3"
|
||||
eth_common "github.com/ethereum/go-ethereum/common"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func TestAcctPendingTransferMsgID(t *testing.T) {
|
||||
tokenBridgeAddr, err := vaa.StringToAddress("0x0290fb167208af455bb137780163b7b7a9a10c16")
|
||||
require.NoError(t, err)
|
||||
|
||||
msg1 := &common.MessagePublication{
|
||||
TxHash: eth_common.HexToHash("0x06f541f5ecfc43407c31587aa6ac3a689e8960f36dc23c332db5510dfc6a4063"),
|
||||
Timestamp: time.Unix(int64(1654516425), 0),
|
||||
Nonce: 123456,
|
||||
Sequence: 789101112131415,
|
||||
EmitterChain: vaa.ChainIDEthereum,
|
||||
EmitterAddress: tokenBridgeAddr,
|
||||
Payload: []byte{},
|
||||
ConsistencyLevel: 16,
|
||||
}
|
||||
|
||||
assert.Equal(t, []byte("ACCT:PXFER:"+"2/0000000000000000000000000290fb167208af455bb137780163b7b7a9a10c16/789101112131415"), acctPendingTransferMsgID(msg1.MessageIDString()))
|
||||
}
|
||||
|
||||
func TestAcctIsPendingTransfer(t *testing.T) {
|
||||
assert.Equal(t, true, acctIsPendingTransfer([]byte("ACCT:PXFER:"+"2/0000000000000000000000000290fb167208af455bb137780163b7b7a9a10c16/789101112131415")))
|
||||
assert.Equal(t, false, acctIsPendingTransfer([]byte("ACCT:PXFER:")))
|
||||
assert.Equal(t, false, acctIsPendingTransfer([]byte("ACCT:PXFER:1")))
|
||||
assert.Equal(t, false, acctIsPendingTransfer([]byte("ACCT:PXFER:1/1/1")))
|
||||
assert.Equal(t, false, acctIsPendingTransfer([]byte("ACCT:PXFER:"+"1/0000000000000000000000000290fb167208af455bb137780163b7b7a9a10c16/")))
|
||||
assert.Equal(t, true, acctIsPendingTransfer([]byte("ACCT:PXFER:"+"1/0000000000000000000000000290fb167208af455bb137780163b7b7a9a10c16/0")))
|
||||
assert.Equal(t, false, acctIsPendingTransfer([]byte("GOV:PENDING:"+"2/0000000000000000000000000290fb167208af455bb137780163b7b7a9a10c16/789101112131415")))
|
||||
assert.Equal(t, false, acctIsPendingTransfer([]byte{0x01, 0x02, 0x03, 0x04}))
|
||||
assert.Equal(t, false, acctIsPendingTransfer([]byte{}))
|
||||
}
|
||||
|
||||
func TestAcctStoreAndDeletePendingTransfers(t *testing.T) {
|
||||
dbPath := t.TempDir()
|
||||
db, err := Open(dbPath)
|
||||
if err != nil {
|
||||
t.Error("failed to open database")
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
tokenBridgeAddr, _ := vaa.StringToAddress("0x0290fb167208af455bb137780163b7b7a9a10c16")
|
||||
require.NoError(t, err)
|
||||
|
||||
msg1 := &common.MessagePublication{
|
||||
TxHash: eth_common.HexToHash("0x06f541f5ecfc43407c31587aa6ac3a689e8960f36dc23c332db5510dfc6a4063"),
|
||||
Timestamp: time.Unix(int64(1654516425), 0),
|
||||
Nonce: 123456,
|
||||
Sequence: 789101112131415,
|
||||
EmitterChain: vaa.ChainIDEthereum,
|
||||
EmitterAddress: tokenBridgeAddr,
|
||||
Payload: []byte{},
|
||||
ConsistencyLevel: 16,
|
||||
}
|
||||
|
||||
msg2 := &common.MessagePublication{
|
||||
TxHash: eth_common.HexToHash("0x06f541f5ecfc43407c31587aa6ac3a689e8960f36dc23c332db5510dfc6a4064"),
|
||||
Timestamp: time.Unix(int64(1654516425), 0),
|
||||
Nonce: 123457,
|
||||
Sequence: 789101112131416,
|
||||
EmitterChain: vaa.ChainIDEthereum,
|
||||
EmitterAddress: tokenBridgeAddr,
|
||||
Payload: []byte{},
|
||||
ConsistencyLevel: 16,
|
||||
}
|
||||
|
||||
err = db.AcctStorePendingTransfer(msg1)
|
||||
require.NoError(t, err)
|
||||
assert.NoError(t, db.rowExistsInDB(acctPendingTransferMsgID(msg1.MessageIDString())))
|
||||
|
||||
err = db.AcctStorePendingTransfer(msg2)
|
||||
require.NoError(t, err)
|
||||
assert.NoError(t, db.rowExistsInDB(acctPendingTransferMsgID(msg2.MessageIDString())))
|
||||
|
||||
err = db.AcctDeletePendingTransfer(msg1.MessageIDString())
|
||||
require.NoError(t, err)
|
||||
assert.Error(t, db.rowExistsInDB(acctPendingTransferMsgID(msg1.MessageIDString())))
|
||||
|
||||
err = db.AcctDeletePendingTransfer(msg2.MessageIDString())
|
||||
require.NoError(t, err)
|
||||
assert.Error(t, db.rowExistsInDB(acctPendingTransferMsgID(msg2.MessageIDString())))
|
||||
|
||||
// Delete something that doesn't exist.
|
||||
msg3 := &common.MessagePublication{
|
||||
TxHash: eth_common.HexToHash("0x06f541f5ecfc43407c31587aa6ac3a689e8960f36dc23c332db5510dfc6a4064"),
|
||||
Timestamp: time.Unix(int64(1654516425), 0),
|
||||
Nonce: 123457,
|
||||
Sequence: 789101112131417,
|
||||
EmitterChain: vaa.ChainIDEthereum,
|
||||
EmitterAddress: tokenBridgeAddr,
|
||||
Payload: []byte{},
|
||||
ConsistencyLevel: 16,
|
||||
}
|
||||
|
||||
err = db.AcctDeletePendingTransfer(msg3.MessageIDString())
|
||||
require.NoError(t, err)
|
||||
assert.Error(t, db.rowExistsInDB(acctPendingTransferMsgID(msg3.MessageIDString())))
|
||||
}
|
||||
|
||||
func TestAcctGetEmptyData(t *testing.T) {
|
||||
dbPath := t.TempDir()
|
||||
db, err := Open(dbPath)
|
||||
if err != nil {
|
||||
t.Error("failed to open database")
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
logger, _ := zap.NewDevelopment()
|
||||
|
||||
pendingTransfers, err := db.AcctGetData(logger)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, len(pendingTransfers))
|
||||
}
|
||||
|
||||
func TestAcctGetData(t *testing.T) {
|
||||
dbPath := t.TempDir()
|
||||
db, err := Open(dbPath)
|
||||
if err != nil {
|
||||
t.Error("failed to open database")
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
logger, _ := zap.NewDevelopment()
|
||||
|
||||
// Store some unrelated junk in the db to make sure it gets skipped.
|
||||
junk := []byte("ABC123")
|
||||
err = db.db.Update(func(txn *badger.Txn) error {
|
||||
if err := txn.Set(junk, junk); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, db.rowExistsInDB(junk))
|
||||
|
||||
tokenBridgeAddr, _ := vaa.StringToAddress("0x0290fb167208af455bb137780163b7b7a9a10c16")
|
||||
require.NoError(t, err)
|
||||
|
||||
msg1 := &common.MessagePublication{
|
||||
TxHash: eth_common.HexToHash("0x06f541f5ecfc43407c31587aa6ac3a689e8960f36dc23c332db5510dfc6a4063"),
|
||||
Timestamp: time.Unix(int64(1654516425), 0),
|
||||
Nonce: 123456,
|
||||
Sequence: 789101112131415,
|
||||
EmitterChain: vaa.ChainIDEthereum,
|
||||
EmitterAddress: tokenBridgeAddr,
|
||||
Payload: []byte{},
|
||||
ConsistencyLevel: 16,
|
||||
}
|
||||
|
||||
msg2 := &common.MessagePublication{
|
||||
TxHash: eth_common.HexToHash("0x06f541f5ecfc43407c31587aa6ac3a689e8960f36dc23c332db5510dfc6a4064"),
|
||||
Timestamp: time.Unix(int64(1654516425), 0),
|
||||
Nonce: 123457,
|
||||
Sequence: 789101112131416,
|
||||
EmitterChain: vaa.ChainIDEthereum,
|
||||
EmitterAddress: tokenBridgeAddr,
|
||||
Payload: []byte{},
|
||||
ConsistencyLevel: 16,
|
||||
}
|
||||
|
||||
err = db.AcctStorePendingTransfer(msg1)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, db.rowExistsInDB(acctPendingTransferMsgID(msg1.MessageIDString())))
|
||||
|
||||
err = db.AcctStorePendingTransfer(msg2)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, db.rowExistsInDB(acctPendingTransferMsgID(msg2.MessageIDString())))
|
||||
|
||||
// Store the same transfer again with an update.
|
||||
msg1a := *msg1
|
||||
msg1a.ConsistencyLevel = 17
|
||||
err = db.AcctStorePendingTransfer(&msg1a)
|
||||
require.NoError(t, err)
|
||||
|
||||
pendingTransfers, err := db.AcctGetData(logger)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 2, len(pendingTransfers))
|
||||
|
||||
assert.Equal(t, msg1a, *pendingTransfers[0])
|
||||
assert.Equal(t, *msg2, *pendingTransfers[1])
|
||||
}
|
|
@ -8,6 +8,7 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/certusone/wormhole/node/pkg/accounting"
|
||||
node_common "github.com/certusone/wormhole/node/pkg/common"
|
||||
"github.com/certusone/wormhole/node/pkg/governor"
|
||||
"github.com/certusone/wormhole/node/pkg/version"
|
||||
|
@ -67,8 +68,26 @@ func signedObservationRequestDigest(b []byte) common.Hash {
|
|||
return ethcrypto.Keccak256Hash(append(signedObservationRequestPrefix, b...))
|
||||
}
|
||||
|
||||
func Run(obsvC chan *gossipv1.SignedObservation, obsvReqC chan *gossipv1.ObservationRequest, obsvReqSendC chan *gossipv1.ObservationRequest, sendC chan []byte, signedInC chan *gossipv1.SignedVAAWithQuorum, priv crypto.PrivKey, gk *ecdsa.PrivateKey, gst *node_common.GuardianSetState, port uint, networkID string, bootstrapPeers string, nodeName string, disableHeartbeatVerify bool, rootCtxCancel context.CancelFunc, gov *governor.ChainGovernor, signedGovCfg chan *gossipv1.SignedChainGovernorConfig,
|
||||
signedGovSt chan *gossipv1.SignedChainGovernorStatus) func(ctx context.Context) error {
|
||||
func Run(
|
||||
obsvC chan *gossipv1.SignedObservation,
|
||||
obsvReqC chan *gossipv1.ObservationRequest,
|
||||
obsvReqSendC chan *gossipv1.ObservationRequest,
|
||||
sendC chan []byte,
|
||||
signedInC chan *gossipv1.SignedVAAWithQuorum,
|
||||
priv crypto.PrivKey,
|
||||
gk *ecdsa.PrivateKey,
|
||||
gst *node_common.GuardianSetState,
|
||||
port uint,
|
||||
networkID string,
|
||||
bootstrapPeers string,
|
||||
nodeName string,
|
||||
disableHeartbeatVerify bool,
|
||||
rootCtxCancel context.CancelFunc,
|
||||
acct *accounting.Accounting,
|
||||
gov *governor.ChainGovernor,
|
||||
signedGovCfg chan *gossipv1.SignedChainGovernorConfig,
|
||||
signedGovSt chan *gossipv1.SignedChainGovernorStatus,
|
||||
) func(ctx context.Context) error {
|
||||
return func(ctx context.Context) (re error) {
|
||||
logger := supervisor.Logger(ctx)
|
||||
|
||||
|
@ -211,6 +230,9 @@ func Run(obsvC chan *gossipv1.SignedObservation, obsvReqC chan *gossipv1.Observa
|
|||
if gov != nil {
|
||||
features = append(features, "governor")
|
||||
}
|
||||
if acct != nil {
|
||||
features = append(features, acct.FeatureString())
|
||||
}
|
||||
|
||||
heartbeat := &gossipv1.Heartbeat{
|
||||
NodeName: nodeName,
|
||||
|
|
|
@ -15,6 +15,7 @@ import (
|
|||
"github.com/ethereum/go-ethereum/crypto"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/certusone/wormhole/node/pkg/accounting"
|
||||
"github.com/certusone/wormhole/node/pkg/common"
|
||||
gossipv1 "github.com/certusone/wormhole/node/pkg/proto/gossip/v1"
|
||||
"github.com/certusone/wormhole/node/pkg/reporter"
|
||||
|
@ -132,6 +133,8 @@ type Processor struct {
|
|||
|
||||
notifier *discord.DiscordNotifier
|
||||
governor *governor.ChainGovernor
|
||||
acct *accounting.Accounting
|
||||
acctReadC <-chan *common.MessagePublication
|
||||
pythnetVaas map[string]PythNetVaaEntry
|
||||
}
|
||||
|
||||
|
@ -154,6 +157,8 @@ func NewProcessor(
|
|||
attestationEvents *reporter.AttestationEventReporter,
|
||||
notifier *discord.DiscordNotifier,
|
||||
g *governor.ChainGovernor,
|
||||
acct *accounting.Accounting,
|
||||
acctReadC <-chan *common.MessagePublication,
|
||||
) *Processor {
|
||||
|
||||
return &Processor{
|
||||
|
@ -181,6 +186,8 @@ func NewProcessor(
|
|||
state: &aggregationState{observationMap{}},
|
||||
ourAddr: crypto.PubkeyToAddress(gk.PublicKey),
|
||||
governor: g,
|
||||
acct: acct,
|
||||
acctReadC: acctReadC,
|
||||
pythnetVaas: make(map[string]PythNetVaaEntry),
|
||||
}
|
||||
}
|
||||
|
@ -194,6 +201,9 @@ func (p *Processor) Run(ctx context.Context) error {
|
|||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
if p.acct != nil {
|
||||
p.acct.Close()
|
||||
}
|
||||
return ctx.Err()
|
||||
case p.gs = <-p.setC:
|
||||
p.logger.Info("guardian set updated",
|
||||
|
@ -206,6 +216,21 @@ func (p *Processor) Run(ctx context.Context) error {
|
|||
continue
|
||||
}
|
||||
}
|
||||
if p.acct != nil {
|
||||
shouldPub, err := p.acct.SubmitObservation(k)
|
||||
if err != nil {
|
||||
return fmt.Errorf("acct: failed to process message `%s`: %w", k.MessageIDString(), err)
|
||||
}
|
||||
if !shouldPub {
|
||||
continue
|
||||
}
|
||||
}
|
||||
p.handleMessage(ctx, k)
|
||||
|
||||
case k := <-p.acctReadC:
|
||||
if p.acct == nil {
|
||||
return fmt.Errorf("acct: received an accounting event when accounting is not configured")
|
||||
}
|
||||
p.handleMessage(ctx, k)
|
||||
case v := <-p.injectC:
|
||||
p.handleInjection(ctx, v)
|
||||
|
@ -223,9 +248,23 @@ func (p *Processor) Run(ctx context.Context) error {
|
|||
}
|
||||
if len(toBePublished) != 0 {
|
||||
for _, k := range toBePublished {
|
||||
if p.acct != nil {
|
||||
shouldPub, err := p.acct.SubmitObservation(k)
|
||||
if err != nil {
|
||||
return fmt.Errorf("acct: failed to process message released by governor `%s`: %w", k.MessageIDString(), err)
|
||||
}
|
||||
if !shouldPub {
|
||||
continue
|
||||
}
|
||||
}
|
||||
p.handleMessage(ctx, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
if p.acct != nil {
|
||||
p.acct.AuditPendingTransfers()
|
||||
}
|
||||
if (p.governor != nil) || (p.acct != nil) {
|
||||
govTimer = time.NewTimer(time.Minute)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,94 @@
|
|||
package wormconn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types"
|
||||
sdktx "github.com/cosmos/cosmos-sdk/types/tx"
|
||||
|
||||
"github.com/btcsuite/btcutil/bech32"
|
||||
wormchain "github.com/wormhole-foundation/wormchain/app"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
)
|
||||
|
||||
// ClienConn represents a connection to a wormhole-chain endpoint, encapsulating
|
||||
// interactions with the chain.
|
||||
//
|
||||
// Once a connection is established, users must call ClientConn.Close to
|
||||
// terminate the connection and free up resources.
|
||||
//
|
||||
// For semantics around ctx use and closing/ending streaming RPCs, please refer
|
||||
// to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream.
|
||||
type ClientConn struct {
|
||||
c *grpc.ClientConn
|
||||
encCfg EncodingConfig
|
||||
privateKey cryptotypes.PrivKey
|
||||
senderAddress string
|
||||
mutex sync.Mutex // Protects the account / sequence number
|
||||
}
|
||||
|
||||
// NewConn creates a new connection to the wormhole-chain instance at `target`.
|
||||
func NewConn(ctx context.Context, target string, privateKey cryptotypes.PrivKey) (*ClientConn, error) {
|
||||
c, err := grpc.DialContext(
|
||||
ctx,
|
||||
target,
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
encCfg := MakeEncodingConfig(wormchain.ModuleBasics)
|
||||
|
||||
senderAddress, err := generateSenderAddress(privateKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &ClientConn{c: c, encCfg: encCfg, privateKey: privateKey, senderAddress: senderAddress}, nil
|
||||
}
|
||||
|
||||
func (c *ClientConn) SenderAddress() string {
|
||||
return c.senderAddress
|
||||
}
|
||||
|
||||
// Close terminates the connection and frees up resources.
|
||||
func (c *ClientConn) Close() {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
c.c.Close()
|
||||
}
|
||||
|
||||
func (c *ClientConn) BroadcastTxResponseToString(txResp *sdktx.BroadcastTxResponse) (string, error) {
|
||||
out, err := c.encCfg.Marshaler.MarshalJSON(txResp)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(out), nil
|
||||
}
|
||||
|
||||
// generateSenderAddress creates the sender address from the private key. It is based on https://pkg.go.dev/github.com/btcsuite/btcutil/bech32#Encode
|
||||
func generateSenderAddress(privateKey cryptotypes.PrivKey) (string, error) {
|
||||
data, err := hex.DecodeString(privateKey.PubKey().Address().String())
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to generate public key, failed to hex decode string: %w", err)
|
||||
}
|
||||
|
||||
conv, err := bech32.ConvertBits(data, 8, 5, true)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to generate public key, failed to convert bits: %w", err)
|
||||
}
|
||||
|
||||
encoded, err := bech32.Encode("wormhole", conv)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to generate public key, bech32 encode failed: %w", err)
|
||||
}
|
||||
|
||||
return encoded, nil
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
// Copyright 2016 All in Bits, Inc
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package wormconn
|
||||
|
||||
import (
|
||||
"github.com/cosmos/cosmos-sdk/client"
|
||||
"github.com/cosmos/cosmos-sdk/codec"
|
||||
"github.com/cosmos/cosmos-sdk/codec/types"
|
||||
"github.com/cosmos/cosmos-sdk/std"
|
||||
"github.com/cosmos/cosmos-sdk/types/module"
|
||||
"github.com/cosmos/cosmos-sdk/x/auth/tx"
|
||||
)
|
||||
|
||||
// EncodingConfig specifies the concrete encoding types to use for a given app.
|
||||
// This is provided for compatibility between protobuf and amino implementations.
|
||||
type EncodingConfig struct {
|
||||
InterfaceRegistry types.InterfaceRegistry
|
||||
Marshaler codec.Codec
|
||||
TxConfig client.TxConfig
|
||||
Amino *codec.LegacyAmino
|
||||
}
|
||||
|
||||
// makeEncodingConfig creates an EncodingConfig for an amino based test configuration.
|
||||
func makeEncodingConfig() EncodingConfig {
|
||||
amino := codec.NewLegacyAmino()
|
||||
interfaceRegistry := types.NewInterfaceRegistry()
|
||||
marshaler := codec.NewProtoCodec(interfaceRegistry)
|
||||
txCfg := tx.NewTxConfig(marshaler, tx.DefaultSignModes)
|
||||
|
||||
return EncodingConfig{
|
||||
InterfaceRegistry: interfaceRegistry,
|
||||
Marshaler: marshaler,
|
||||
TxConfig: txCfg,
|
||||
Amino: amino,
|
||||
}
|
||||
}
|
||||
|
||||
// MakeEncodingConfig creates an EncodingConfig for testing
|
||||
func MakeEncodingConfig(moduleBasics module.BasicManager) EncodingConfig {
|
||||
encodingConfig := makeEncodingConfig()
|
||||
std.RegisterLegacyAminoCodec(encodingConfig.Amino)
|
||||
std.RegisterInterfaces(encodingConfig.InterfaceRegistry)
|
||||
moduleBasics.RegisterLegacyAminoCodec(encodingConfig.Amino)
|
||||
moduleBasics.RegisterInterfaces(encodingConfig.InterfaceRegistry)
|
||||
return encodingConfig
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
package wormconn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
txclient "github.com/cosmos/cosmos-sdk/client/tx"
|
||||
sdktypes "github.com/cosmos/cosmos-sdk/types"
|
||||
sdktx "github.com/cosmos/cosmos-sdk/types/tx"
|
||||
"github.com/cosmos/cosmos-sdk/types/tx/signing"
|
||||
authsigning "github.com/cosmos/cosmos-sdk/x/auth/signing"
|
||||
auth "github.com/cosmos/cosmos-sdk/x/auth/types"
|
||||
)
|
||||
|
||||
func (c *ClientConn) SignAndBroadcastTx(ctx context.Context, msg sdktypes.Msg) (*sdktx.BroadcastTxResponse, error) {
|
||||
// Lock to protect the wallet sequence number.
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
|
||||
authClient := auth.NewQueryClient(c.c)
|
||||
accountQuery := &auth.QueryAccountRequest{
|
||||
Address: c.senderAddress,
|
||||
}
|
||||
resp, err := authClient.Account(ctx, accountQuery)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch account: %w", err)
|
||||
}
|
||||
|
||||
var account auth.AccountI
|
||||
if err := c.encCfg.InterfaceRegistry.UnpackAny(resp.Account, &account); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal account info: %w", err)
|
||||
}
|
||||
|
||||
builder := c.encCfg.TxConfig.NewTxBuilder()
|
||||
if err := builder.SetMsgs(msg); err != nil {
|
||||
return nil, fmt.Errorf("failed to add message to builder: %w", err)
|
||||
}
|
||||
builder.SetGasLimit(2000000) // TODO: Maybe simulate and use the result
|
||||
|
||||
// The tx needs to be signed in 2 passes: first we populate the SignerInfo
|
||||
// inside the TxBuilder and then sign the payload.
|
||||
sequence := account.GetSequence()
|
||||
sig := signing.SignatureV2{
|
||||
PubKey: c.privateKey.PubKey(),
|
||||
Data: &signing.SingleSignatureData{
|
||||
SignMode: c.encCfg.TxConfig.SignModeHandler().DefaultMode(),
|
||||
Signature: nil,
|
||||
},
|
||||
Sequence: sequence,
|
||||
}
|
||||
if err := builder.SetSignatures(sig); err != nil {
|
||||
return nil, fmt.Errorf("failed to set SignerInfo: %w", err)
|
||||
}
|
||||
|
||||
signerData := authsigning.SignerData{
|
||||
ChainID: "wormchain",
|
||||
AccountNumber: account.GetAccountNumber(),
|
||||
Sequence: sequence,
|
||||
}
|
||||
|
||||
sig, err = txclient.SignWithPrivKey(
|
||||
c.encCfg.TxConfig.SignModeHandler().DefaultMode(),
|
||||
signerData,
|
||||
builder,
|
||||
c.privateKey,
|
||||
c.encCfg.TxConfig,
|
||||
sequence,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to sign tx: %w", err)
|
||||
}
|
||||
if err := builder.SetSignatures(sig); err != nil {
|
||||
return nil, fmt.Errorf("failed to update tx signature: %w", err)
|
||||
}
|
||||
|
||||
txBytes, err := c.encCfg.TxConfig.TxEncoder()(builder.GetTx())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal tx: %w", err)
|
||||
}
|
||||
|
||||
client := sdktx.NewServiceClient(c.c)
|
||||
|
||||
// Returns *BroadcastTxResponse
|
||||
txResp, err := client.BroadcastTx(
|
||||
ctx,
|
||||
&sdktx.BroadcastTxRequest{
|
||||
Mode: sdktx.BroadcastMode_BROADCAST_MODE_BLOCK,
|
||||
TxBytes: txBytes,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to broadcast tx: %w", err)
|
||||
}
|
||||
|
||||
return txResp, nil
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package wormconn
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/cosmos/cosmos-sdk/crypto"
|
||||
cryptotypes "github.com/cosmos/cosmos-sdk/crypto/types"
|
||||
)
|
||||
|
||||
func LoadWormchainPrivKey(path string, passPhrase string) (cryptotypes.PrivKey, error) {
|
||||
armor, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
key, _, err := crypto.UnarmorDecryptPrivKey(string(armor), passPhrase)
|
||||
return key, err
|
||||
}
|
|
@ -15,6 +15,9 @@ spec:
|
|||
- name: rest
|
||||
port: 1317
|
||||
protocol: TCP
|
||||
- name: cosmos-rpc
|
||||
port: 9090
|
||||
protocol: TCP
|
||||
selector:
|
||||
app: guardian-validator
|
||||
---
|
||||
|
@ -54,6 +57,9 @@ spec:
|
|||
- containerPort: 1317
|
||||
name: rest
|
||||
protocol: TCP
|
||||
- containerPort: 9090
|
||||
name: cosmos-rpc
|
||||
protocol: TCP
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
port: 26657
|
||||
|
|
Loading…
Reference in New Issue