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:
bruce-riley 2023-01-16 06:33:01 -06:00 committed by GitHub
parent 8777c22d32
commit 499c8424e4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 4803 additions and 396 deletions

View File

@ -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 = [],

View File

@ -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==

View File

@ -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/

View File

@ -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)
}

View File

@ -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
}

View File

@ -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

View File

@ -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
}

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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-----

View File

@ -0,0 +1,10 @@
-----BEGIN TENDERMINT PRIVATE KEY-----
kdf: bcrypt
salt: 4768766A78FD7FAB301BF9391F0CF63F
type: secp256k1
dlFn7VjFM6FrcbGZT4VxNrFQ7IHPKdrUPBE6+io2+L4TVjr+9zcHA1ww3nmkj5Qe
uRzBV242yGSsxMM2YrB6CVWw9ZYrIcrEzK5sAnI=
=pvhy
-----END TENDERMINT PRIVATE KEY-----

View File

@ -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'
}
*/

View File

@ -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))
}
}
}

View File

@ -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))
}

View File

@ -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",
})
)

View File

@ -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
}

View File

@ -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))
}
}

View File

@ -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)
}

View File

@ -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())
}

View File

@ -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)

111
node/pkg/db/accounting.go Normal file
View File

@ -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
}

View File

@ -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])
}

View File

@ -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,

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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