Skip to content

Commit 1d4a21e

Browse files
authored
feat: Verify transfer addresses in Observer (#7943) (#7956)
1 parent ffa99ca commit 1d4a21e

File tree

7 files changed

+466
-35
lines changed

7 files changed

+466
-35
lines changed

x/bridge/observer/bitcoin/bitcoin.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
55
"encoding/hex"
66
"errors"
77
"fmt"
8-
"slices"
98
"strings"
109
"sync/atomic"
1110
"time"
@@ -205,7 +204,7 @@ func (b *ChainClient) processTx(height uint64, tx *btcjson.TxRawResult) (observe
205204

206205
memo, contains := getMemo(tx)
207206
if !contains {
208-
return observer.Transfer{}, false, fmt.Errorf("failed to get Tx memo")
207+
return observer.Transfer{}, false, fmt.Errorf("malformed Tx memo")
209208
}
210209

211210
return observer.Transfer{
@@ -222,7 +221,7 @@ func (b *ChainClient) processTx(height uint64, tx *btcjson.TxRawResult) (observe
222221

223222
// isTxRelevant checks if a tx contains transfers to the BTC vault and calculates the total amount to transfer.
224223
// This method goes through all the Vouts and accumulates their values if Vouts.account[0] == vault.
225-
// Returns true is the tx is not relevant. Otherwise, false and the total amount to transfer.
224+
// Returns false if the tx is not relevant. Otherwise, true and the total amount to transfer.
226225
func (b *ChainClient) isTxRelevant(tx *btcjson.TxRawResult) (math.Uint, bool) {
227226
var amount = sdk.NewUint(0)
228227
for _, vout := range tx.Vout {
@@ -238,7 +237,7 @@ func (b *ChainClient) isTxRelevant(tx *btcjson.TxRawResult) (math.Uint, bool) {
238237
continue
239238
}
240239

241-
if slices.Contains(addresses, b.vaultAddr) {
240+
if addresses[0] == b.vaultAddr {
242241
a, err := getAmount(vout)
243242
if err != nil {
244243
continue
@@ -273,7 +272,10 @@ func getMemo(tx *btcjson.TxRawResult) (string, bool) {
273272
// TODO: log?
274273
continue
275274
}
276-
// TODO: verify the memo format
275+
_, err = sdk.AccAddressFromBech32(string(decoded))
276+
if err != nil {
277+
continue
278+
}
277279
return string(decoded), true
278280
}
279281
}

x/bridge/observer/bitcoin/bitcoin_test.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"github.com/cometbft/cometbft/libs/log"
1919
"github.com/stretchr/testify/require"
2020

21+
_ "github.com/osmosis-labs/osmosis/v24/app/params" // init Osmosis address prefixes
2122
"github.com/osmosis-labs/osmosis/v24/x/bridge/observer"
2223
"github.com/osmosis-labs/osmosis/v24/x/bridge/observer/bitcoin"
2324
bridgetypes "github.com/osmosis-labs/osmosis/v24/x/bridge/types"
@@ -104,9 +105,16 @@ func TestListenOutboundTransfer(t *testing.T) {
104105
err = b.Start(ctx)
105106
require.NoError(t, err)
106107

107-
// We expect Observer to observe 1 block with 2 Txs
108-
// Only 1 Tx is sent to our vault address,
109-
// so we should receive only 1 TxIn
108+
// We expect Observer to observe 1 block with 8 Txs:
109+
// - tx to our vault but without memo
110+
// - tx to our vault with memo with invalid address
111+
// - tx to our vault but with zero tokens
112+
// - tx to our vault with invalid script type for output Vout
113+
// - tx to our vault with invalid script type for memo Vout
114+
// - tx to our vault with invalid tokens amount
115+
// - unrelated tx
116+
// + valid tx to our vault
117+
// So, we should receive only 1 Transfer
110118
txs := b.ListenOutboundTransfer()
111119
var out observer.Transfer
112120
require.Eventually(t, func() bool {

x/bridge/observer/bitcoin/test_responses/block_verbose_tx.json

Lines changed: 336 additions & 1 deletion
Large diffs are not rendered by default.

x/bridge/observer/osmosis/osmosis.go

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ import (
88

99
errorsmod "cosmossdk.io/errors"
1010
"cosmossdk.io/math"
11+
"github.com/btcsuite/btcd/btcutil"
12+
btcdchaincfg "github.com/btcsuite/btcd/chaincfg"
13+
abcitypes "github.com/cometbft/cometbft/abci/types"
1114
"github.com/cometbft/cometbft/libs/log"
1215
rpchttp "github.com/cometbft/cometbft/rpc/client/http"
1316
coretypes "github.com/cometbft/cometbft/rpc/core/types"
@@ -19,6 +22,13 @@ import (
1922
bridgetypes "github.com/osmosis-labs/osmosis/v24/x/bridge/types"
2023
)
2124

25+
type Mode = string
26+
27+
const (
28+
ModeMainnet Mode = "mainnet"
29+
ModeTestnet Mode = "testnet"
30+
)
31+
2232
var (
2333
ModuleName = "osmosis-chain"
2434
OsmoGasLimit = uint64(200000)
@@ -28,6 +38,7 @@ var (
2838

2939
type ChainClient struct {
3040
logger log.Logger
41+
mode Mode
3142
osmoClient *Client
3243
cometRpc *rpchttp.HTTP
3344
stopChan chan struct{}
@@ -40,13 +51,15 @@ type ChainClient struct {
4051
// NewChainClient returns new instance of `Osmosis`
4152
func NewChainClient(
4253
logger log.Logger,
54+
mode Mode,
4355
osmoClient *Client,
4456
cometRpc *rpchttp.HTTP,
4557
txConfig cosmosclient.TxConfig,
4658
signerAddr string,
4759
) *ChainClient {
4860
return &ChainClient{
4961
logger: logger.With("module", ModuleName),
62+
mode: mode,
5063
osmoClient: osmoClient,
5164
cometRpc: cometRpc,
5265
stopChan: make(chan struct{}),
@@ -114,14 +127,17 @@ func (c *ChainClient) SignalInboundTransfer(ctx context.Context, in observer.Tra
114127
if err != nil {
115128
return errorsmod.Wrapf(err, "Failed to sign tx for inbound transfer %s", in.Id)
116129
}
117-
_, err = c.osmoClient.BroadcastTx(ctx, bytes)
130+
resp, err := c.osmoClient.BroadcastTx(ctx, bytes)
118131
if err != nil {
119132
return errorsmod.Wrapf(
120133
err,
121134
"Failed to broadcast tx to Osmosis for inbound transfer %s",
122135
in.Id,
123136
)
124137
}
138+
if resp.Code != abcitypes.CodeTypeOK {
139+
return fmt.Errorf("Tx for inbound transfer %s failed inside Osmosis: %s", in.Id, resp.RawLog)
140+
}
125141
return nil
126142
}
127143

@@ -170,6 +186,7 @@ func (c *ChainClient) processNewBlockTxs(ctx context.Context, height uint64, txs
170186
))
171187
continue
172188
}
189+
173190
if res.IsErr() {
174191
continue
175192
}
@@ -180,6 +197,21 @@ func (c *ChainClient) processNewBlockTxs(ctx context.Context, height uint64, txs
180197
continue
181198
}
182199

200+
err = verifyOutboundDestAddress(
201+
c.mode,
202+
observer.ChainId(outbound.AssetId.SourceChain),
203+
outbound.DestAddr,
204+
)
205+
if err != nil {
206+
c.logger.Error(fmt.Sprintf(
207+
"Invalid outbound destination address in Tx %s, block %d: %s",
208+
txHash,
209+
height,
210+
err.Error(),
211+
))
212+
continue
213+
}
214+
183215
out := outboundTransferFromMsg(
184216
height,
185217
txHash,
@@ -218,6 +250,21 @@ func outboundTransferFromMsg(
218250
}
219251
}
220252

253+
func verifyOutboundDestAddress(mode Mode, chainId observer.ChainId, addr string) error {
254+
switch chainId {
255+
case observer.ChainIdBitcoin:
256+
switch mode {
257+
case ModeMainnet:
258+
_, err := btcutil.DecodeAddress(addr, &btcdchaincfg.MainNetParams)
259+
return err
260+
case ModeTestnet:
261+
_, err := btcutil.DecodeAddress(addr, &btcdchaincfg.TestNet3Params)
262+
return err
263+
}
264+
}
265+
return fmt.Errorf("Unsupported outbound destination chain: %s", chainId)
266+
}
267+
221268
// Height returns current height of the chain
222269
func (c *ChainClient) Height(context.Context) (uint64, error) {
223270
return c.lastObservedHeight.Load(), nil

x/bridge/observer/osmosis/osmosis_test.go

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package osmosis_test
33
import (
44
"context"
55
"encoding/json"
6+
"io"
67
"net/http"
78
"net/http/httptest"
89
"os"
@@ -100,6 +101,7 @@ func NewOsmosisTestSuite(t *testing.T, ctx context.Context) OsmosisTestSuite {
100101
client := osmosis.NewClient(ChainId, conn, kr, app.GetEncodingConfig().TxConfig)
101102
o := osmosis.NewChainClient(
102103
log.NewNopLogger(),
104+
osmosis.ModeTestnet,
103105
client,
104106
cometRpc,
105107
app.GetEncodingConfig().TxConfig,
@@ -134,13 +136,19 @@ func readNewBlockEvent(t *testing.T, path string) coretypes.ResultEvent {
134136
return result
135137
}
136138

137-
func readTxCheck(t *testing.T, path string) abci.ResponseCheckTx {
139+
func readTxCheckBytes(t *testing.T, id int, path string) []byte {
138140
dataStr, err := os.ReadFile(path)
139141
require.NoError(t, err)
140142
result := abci.ResponseCheckTx{}
141143
err = json.Unmarshal([]byte(dataStr), &result)
142144
require.NoError(t, err)
143-
return result
145+
checkResultsResp := cmtrpctypes.NewRPCSuccessResponse(
146+
cmtrpctypes.JSONRPCIntID(id),
147+
result,
148+
)
149+
checkResultsRaw, err := json.Marshal(checkResultsResp)
150+
require.NoError(t, err)
151+
return checkResultsRaw
144152
}
145153

146154
func success(t *testing.T) http.HandlerFunc {
@@ -150,7 +158,7 @@ func success(t *testing.T) http.HandlerFunc {
150158
c, err := upgrader.Upgrade(w, r, nil)
151159
require.NoError(t, err)
152160
defer c.Close()
153-
newBlock := readNewBlockEvent(t, "./test_events/new_block_success.json")
161+
newBlock := readNewBlockEvent(t, "./test_events/new_block.json")
154162
newBlockResp := cmtrpctypes.NewRPCSuccessResponse(
155163
cmtrpctypes.JSONRPCIntID(1),
156164
newBlock,
@@ -160,14 +168,25 @@ func success(t *testing.T) http.HandlerFunc {
160168
err = c.WriteMessage(1, newBlockRaw)
161169
require.NoError(t, err)
162170
case http.MethodPost:
163-
checkResults := readTxCheck(t, "./test_events/tx_check_success.json")
164-
checkResultsResp := cmtrpctypes.NewRPCSuccessResponse(
165-
cmtrpctypes.JSONRPCIntID(0),
166-
checkResults,
167-
)
168-
checkResultsRaw, err := json.Marshal(checkResultsResp)
171+
bytes, err := io.ReadAll(r.Body)
172+
require.NoError(t, err)
173+
require.NoError(t, r.Body.Close())
174+
var req cmtrpctypes.RPCRequest
175+
err = json.Unmarshal(bytes, &req)
169176
require.NoError(t, err)
170-
_, err = w.Write(checkResultsRaw)
177+
jsonId, ok := req.ID.(cmtrpctypes.JSONRPCIntID)
178+
require.True(t, ok)
179+
id := int(jsonId)
180+
181+
var resp []byte
182+
switch id {
183+
case 1:
184+
resp = readTxCheckBytes(t, id, "./test_events/tx_check_error.json")
185+
default:
186+
resp = readTxCheckBytes(t, id, "./test_events/tx_check_success.json")
187+
}
188+
189+
_, err = w.Write(resp)
171190
require.NoError(t, err)
172191
default:
173192
t.Fatal("Unexpected request method", r.Method)
@@ -282,6 +301,11 @@ func TestListenOutboundTransfer(t *testing.T) {
282301
ots.Start(t, ctx)
283302
defer ots.Stop(t, ctx)
284303

304+
// We expect to observe 1 block with 3 Txs each with a `MsgOutboundTransfer` message:
305+
// - valid tx to BTC address
306+
// - failed tx
307+
// - tx with invalid destination address
308+
// So, we should to receive only 1 Transfer
285309
transfers := ots.o.ListenOutboundTransfer()
286310
var transfer observer.Transfer
287311
require.Eventually(t, func() bool {
@@ -292,8 +316,8 @@ func TestListenOutboundTransfer(t *testing.T) {
292316
expTransfer := observer.Transfer{
293317
SrcChain: observer.ChainIdOsmosis,
294318
DstChain: observer.ChainIdBitcoin,
295-
Id: "8593aa191651f6a3e2978fb5334b3e5b1e20abd72ad539f15c76f241fa696d3e",
296-
Height: 881,
319+
Id: "8eb4b69be7144690f82a4e1485f4b85d23adc5267db5d3dab7affae57c8ce2a4",
320+
Height: 2801,
297321
Sender: "osmo1pldlhnwegsj3lqkarz0e4flcsay3fuqgkd35ww",
298322
To: "2Mt1ttL5yffdfCGxpfxmceNE4CRUcAsBbgQ",
299323
Asset: bridgetypes.DefaultBitcoinDenomName,

x/bridge/observer/osmosis/test_events/new_block_success.json renamed to x/bridge/observer/osmosis/test_events/new_block.json

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,49 +9,51 @@
99
"block": "11"
1010
},
1111
"chain_id": "my-test-chain",
12-
"height": "881",
13-
"time": "2024-04-01T14:17:10.715933958Z",
12+
"height": "2801",
13+
"time": "2024-04-03T09:47:40.739349321Z",
1414
"last_block_id": {
15-
"hash": "55E2A190F9F1877A1C390F557E3208E45F2C5BD2F782C76E03ACB4266B17A359",
15+
"hash": "82A6EA06C84B7BF17D3173E1232B5CE820DFC7D7417B2E141B0CDB8F855F3CA1",
1616
"parts": {
1717
"total": 1,
18-
"hash": "D8E296FACC516BD1010A1BB4C9ABCCC512E2821873956839AA17C10FA63A911E"
18+
"hash": "0D5C54C6C15823FFE26B70B62BDCC0C5402C41804FD3AE1F3A1728E84AEEDC5C"
1919
}
2020
},
21-
"last_commit_hash": "428E15CBA9DB1089C9FC9BACE274CEA0F2664115F365D4AC3069425BDAEE4035",
22-
"data_hash": "CF137839AB61FD2046F0553A2F986504D2F37497120456D03391A9FF21992993",
21+
"last_commit_hash": "1F5E3CFD4001F02DFC16457F8E4B297EF43FF3AA9F989BD8F95F0F7E13990355",
22+
"data_hash": "70CF44DFF1E91F23C1474508793250E0E97BB5647D6E495A7378438E67389DB7",
2323
"validators_hash": "1E811B4CEEF78E840C253AB5928C670A6A6B389A231496E9ABF8084FD6206FAF",
2424
"next_validators_hash": "1E811B4CEEF78E840C253AB5928C670A6A6B389A231496E9ABF8084FD6206FAF",
2525
"consensus_hash": "048091BC7DDC283F77BFBF91D73C44DA58C3DF8A9CBC867405D8B7F3DAADA22F",
26-
"app_hash": "281B4F7AFCF115D4F72A47A1BDC03A59F180C32E9719FED4D48C3A6FCC2C7DAF",
26+
"app_hash": "7573ABD8364FF3B5F118F7A64FF1BE39268CA710A72952021CE7CB1C267F4B89",
2727
"last_results_hash": "E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855",
2828
"evidence_hash": "E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855",
2929
"proposer_address": "B81ECB7226036A4B8ACFC81EBDF24A2527D25BE8"
3030
},
3131
"data": {
3232
"txs": [
33-
"CpgBCpUBCisvb3Ntb3Npcy5icmlkZ2UudjFiZXRhMS5Nc2dPdXRib3VuZFRyYW5zZmVyEmYKK29zbW8xcGxkbGhud2Vnc2ozbHFrYXJ6MGU0Zmxjc2F5M2Z1cWdrZDM1d3cSIzJNdDF0dEw1eWZmZGZDR3hwZnhtY2VORTRDUlVjQXNCYmdRGg4KB2JpdGNvaW4SA2J0YyICMTASZwpQCkYKHy9jb3Ntb3MuY3J5cHRvLnNlY3AyNTZrMS5QdWJLZXkSIwohAqGxGGMLwZu1ElZA/wZ9hdIPpYUzRWaKUtzzwx6sRbZlEgQKAggBGAgSEwoNCgVzdGFrZRIEMTAwMBDAmgwaQGATrlA3NQt5cMVeUjt1i0YngPv2SP3cdob0wJlZekreByBudAJnxJpWhLCuWKkrAZVCaXtHrV4SoYUsvHfKhMg="
33+
"CqABCp0BCisvb3Ntb3Npcy5icmlkZ2UudjFiZXRhMS5Nc2dPdXRib3VuZFRyYW5zZmVyEm4KK29zbW8xcGxkbGhud2Vnc2ozbHFrYXJ6MGU0Zmxjc2F5M2Z1cWdrZDM1d3cSK29zbW8xbWZrbDRwOTJscXZsZjdmaDVycGRzM2FmdnBhc2U3OGZ6NWgzNmwaDgoHYml0Y29pbhIDYnRjIgIxMBJnClAKRgofL2Nvc21vcy5jcnlwdG8uc2VjcDI1NmsxLlB1YktleRIjCiECobEYYwvBm7USVkD/Bn2F0g+lhTNFZopS3PPDHqxFtmUSBAoCCAEYDRITCg0KBXN0YWtlEgQxMDAwEMCaDBpAeyBpiYpl0iBgBB2g5OZyvKSgYT9pOrW4uxtsVV/IZ1pvbA1hcjXKNk9IKyL8Gcv6eS05ZXSbBmwtBY3L7Dey1w==",
34+
"CpgBCpUBCisvb3Ntb3Npcy5icmlkZ2UudjFiZXRhMS5Nc2dPdXRib3VuZFRyYW5zZmVyEmYKK29zbW8xcGxkbGhud2Vnc2ozbHFrYXJ6MGU0Zmxjc2F5M2Z1cWdrZDM1d3cSIzJNdDF0dEw1eWZmZGZDR3hwZnhtY2VORTRDUlVjQXNCYmdRGg4KB2JpdGNvaW4SA2J0YyICMTASZwpQCkYKHy9jb3Ntb3MuY3J5cHRvLnNlY3AyNTZrMS5QdWJLZXkSIwohAqGxGGMLwZu1ElZA/wZ9hdIPpYUzRWaKUtzzwx6sRbZlEgQKAggBGA4SEwoNCgVzdGFrZRIEMTAwMBDAmgwaQIvlf1BifOQRKamSTBaUK05SUMzybrfYfNYH3vvEBpuFW/w2H4p7YIPYKnhsdYv7D1pZCpFHfNZ5rScRIjnFlwg=",
35+
"CpgBCpUBCisvb3Ntb3Npcy5icmlkZ2UudjFiZXRhMS5Nc2dPdXRib3VuZFRyYW5zZmVyEmYKK29zbW8xcGxkbGhud2Vnc2ozbHFrYXJ6MGU0Zmxjc2F5M2Z1cWdrZDM1d3cSIzJNdDF0dEw1eWZmZGZDR3hwZnhtY2VORTRDUlVjQXNCYmdRGg4KB2JpdGNvaW4SA2J0YyICMTASZwpQCkYKHy9jb3Ntb3MuY3J5cHRvLnNlY3AyNTZrMS5QdWJLZXkSIwohAqGxGGMLwZu1ElZA/wZ9hdIPpYUzRWaKUtzzwx6sRbZlEgQKAggBGA8SEwoNCgVzdGFrZRIEMTAwMBDAmgwaQLlOXD9HIHgLrYbxtCyViEoaqlosMgqqGhHux4fQHPZ0E/rfxPNhq4lpJ9LzW4u2WKsmNsEMLYfMtIwd4IgScy4="
3436
]
3537
},
3638
"evidence": {
3739
"evidence": null
3840
},
3941
"last_commit": {
40-
"height": "880",
42+
"height": "2800",
4143
"round": 0,
4244
"block_id": {
43-
"hash": "55E2A190F9F1877A1C390F557E3208E45F2C5BD2F782C76E03ACB4266B17A359",
45+
"hash": "82A6EA06C84B7BF17D3173E1232B5CE820DFC7D7417B2E141B0CDB8F855F3CA1",
4446
"parts": {
4547
"total": 1,
46-
"hash": "D8E296FACC516BD1010A1BB4C9ABCCC512E2821873956839AA17C10FA63A911E"
48+
"hash": "0D5C54C6C15823FFE26B70B62BDCC0C5402C41804FD3AE1F3A1728E84AEEDC5C"
4749
}
4850
},
4951
"signatures": [
5052
{
5153
"block_id_flag": 2,
5254
"validator_address": "B81ECB7226036A4B8ACFC81EBDF24A2527D25BE8",
53-
"timestamp": "2024-04-01T14:17:10.715933958Z",
54-
"signature": "yv+UN4V8RfB7JceRW2QrWLOlIyZwlp5421aAC9UH6BL3OBIIKHDGhy5nPFDQ19BQIreejLC1LSkbwhDSjMhGBw=="
55+
"timestamp": "2024-04-03T09:47:40.739349321Z",
56+
"signature": "qyR90k0Xw6WnZ8lORWtzcS3JJcFoA5tc6ahG32TXle/frx6xUgZ3gHRxwA/Ofy2CEnB5Y0MGkDYg9Jfy18ZQAw=="
5557
}
5658
]
5759
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"code": 32,
3+
"data": null,
4+
"log": "account sequence mismatch, expected 10, got 9: incorrect account sequence",
5+
"info": "",
6+
"gas_wanted": "200000",
7+
"gas_used": "39457",
8+
"events": [],
9+
"codespace": "sdk",
10+
"sender": "",
11+
"priority": "0",
12+
"mempoolError": ""
13+
}

0 commit comments

Comments
 (0)