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