proof of concept
This commit is contained in:
parent
28215e5ea7
commit
4985a5635a
|
@ -0,0 +1,72 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/certusone/tpuproxy/pkg/tpu"
|
||||
"github.com/gagliardetto/solana-go"
|
||||
"github.com/google/gopacket"
|
||||
"github.com/google/gopacket/layers"
|
||||
"github.com/google/gopacket/pcap"
|
||||
"sort"
|
||||
)
|
||||
|
||||
// pkcon install libpcap-devel
|
||||
|
||||
// readPCAP reads a PCAP file and returns a channel of packets.
|
||||
func readPCAP(file string) chan []byte {
|
||||
packets := make(chan []byte)
|
||||
go func() {
|
||||
defer close(packets)
|
||||
handle, err := pcap.OpenOffline(file)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer handle.Close()
|
||||
packetSource := gopacket.NewPacketSource(handle, handle.LinkType())
|
||||
for packet := range packetSource.Packets() {
|
||||
udpLayer := packet.Layer(layers.LayerTypeUDP)
|
||||
if udpLayer == nil {
|
||||
continue
|
||||
}
|
||||
packets <- udpLayer.LayerPayload()
|
||||
}
|
||||
}()
|
||||
return packets
|
||||
}
|
||||
|
||||
func main() {
|
||||
packets := readPCAP("fixtures/tpu.pcap")
|
||||
|
||||
signerCount := make(map[solana.PublicKey]uint)
|
||||
|
||||
for p := range packets {
|
||||
tx, err := tpu.ParseTx(p)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
continue
|
||||
}
|
||||
|
||||
ok := tpu.VerifyTxSig(tx)
|
||||
if !ok {
|
||||
fmt.Printf("bad signature on %s", tx.Signatures[0])
|
||||
continue
|
||||
}
|
||||
|
||||
signers := tpu.ExtractSigners(tx)
|
||||
for _, signer := range signers {
|
||||
signerCount[signer]++
|
||||
}
|
||||
}
|
||||
|
||||
// sort by count
|
||||
var keys []solana.PublicKey
|
||||
for k := range signerCount {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Slice(keys, func(i, j int) bool {
|
||||
return signerCount[keys[i]] > signerCount[keys[j]]
|
||||
})
|
||||
for _, k := range keys {
|
||||
fmt.Printf("%s\t%d\n", k, signerCount[k])
|
||||
}
|
||||
}
|
|
@ -0,0 +1,133 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/certusone/tpuproxy/pkg/endpoints"
|
||||
"github.com/certusone/tpuproxy/pkg/tpu"
|
||||
"github.com/google/gopacket"
|
||||
"github.com/google/gopacket/layers"
|
||||
"github.com/google/gopacket/pcap"
|
||||
"k8s.io/klog/v2"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type packet struct {
|
||||
data []byte
|
||||
port uint16
|
||||
src net.IP
|
||||
}
|
||||
|
||||
func readPacketsFromInterface(iface string, ports []uint16, dst net.IP) (chan packet, error) {
|
||||
// bpf filter
|
||||
var filter string
|
||||
for _, port := range ports {
|
||||
filter += fmt.Sprintf(" or dst port %d", port)
|
||||
}
|
||||
filter = fmt.Sprintf("udp and dst host %s and (%s)", dst.String(), filter[4:])
|
||||
|
||||
klog.Info("filter: ", filter)
|
||||
|
||||
handle, err := pcap.OpenLive(iface, 1600, false, pcap.BlockForever)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// set filter
|
||||
err = handle.SetBPFFilter(filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
packets := make(chan packet)
|
||||
go func() {
|
||||
defer close(packets)
|
||||
packetSource := gopacket.NewPacketSource(handle, handle.LinkType())
|
||||
for p := range packetSource.Packets() {
|
||||
udpLayer := p.Layer(layers.LayerTypeUDP)
|
||||
if udpLayer == nil {
|
||||
continue
|
||||
}
|
||||
packets <- packet{
|
||||
data: udpLayer.(*layers.UDP).Payload,
|
||||
port: uint16(udpLayer.(*layers.UDP).DstPort),
|
||||
}
|
||||
}
|
||||
}()
|
||||
return packets, nil
|
||||
}
|
||||
|
||||
var (
|
||||
flagIface = flag.String("iface", "", "interface to read packets from")
|
||||
flagPorts = flag.String("ports", "", "destination ports to sniff (comma-separated), asks local RPC if empty")
|
||||
)
|
||||
|
||||
// getInterfaceIP returns the primary IP address associated with the given interface.
|
||||
func getInterfaceIP(iface string) (net.IP, error) {
|
||||
ifaceAddr, err := net.InterfaceByName(iface)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
addrs, err := ifaceAddr.Addrs()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, addr := range addrs {
|
||||
if ip, ok := addr.(*net.IPNet); ok && ip.IP.To4() != nil {
|
||||
return ip.IP, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("no IP address found for interface %s", iface)
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
if *flagIface == "" {
|
||||
klog.Exit("-iface is required")
|
||||
}
|
||||
|
||||
dst, err := getInterfaceIP(*flagIface)
|
||||
if err != nil {
|
||||
klog.Exit("failed to get IP: ", err)
|
||||
}
|
||||
|
||||
klog.Infof("interface %s has primary IP %s", *flagIface, dst)
|
||||
|
||||
ports := make([]uint16, 0)
|
||||
|
||||
if *flagPorts == "" {
|
||||
klog.Infof("no ports specified, asking local RPC for ports")
|
||||
ports, err = endpoints.GetNodeTPUPorts(context.Background(), endpoints.RPCLocalhost, dst)
|
||||
if err != nil {
|
||||
klog.Exit("failed to get ports: ", err)
|
||||
}
|
||||
klog.Infof("found ports: %v", ports)
|
||||
} else {
|
||||
for _, port := range strings.Split(*flagPorts, ",") {
|
||||
p, err := strconv.ParseUint(port, 10, 16)
|
||||
if err != nil {
|
||||
klog.Exit("failed to parse port: ", err)
|
||||
}
|
||||
ports = append(ports, uint16(p))
|
||||
}
|
||||
}
|
||||
|
||||
packets, err := readPacketsFromInterface(*flagIface, ports, dst)
|
||||
if err != nil {
|
||||
klog.Exit("error reading packets: ", err)
|
||||
}
|
||||
|
||||
for p := range packets {
|
||||
tx, err := tpu.ParseTx(p.data)
|
||||
if err != nil {
|
||||
klog.Warning("port %d error parsing tx: ", p.port, err)
|
||||
continue
|
||||
}
|
||||
|
||||
signers := tpu.ExtractSigners(tx)
|
||||
klog.Infof("port %d sig %s signers %v", p.port, tx.Signatures[0], signers)
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
*.pcap
|
|
@ -0,0 +1,46 @@
|
|||
module github.com/certusone/tpuproxy
|
||||
|
||||
go 1.17
|
||||
|
||||
require (
|
||||
github.com/gagliardetto/binary v0.5.0
|
||||
github.com/gagliardetto/solana-go v1.0.2
|
||||
github.com/google/gopacket v1.1.19
|
||||
k8s.io/klog/v2 v2.30.0
|
||||
)
|
||||
|
||||
require (
|
||||
contrib.go.opencensus.io/exporter/stackdriver v0.13.4 // indirect
|
||||
filippo.io/edwards25519 v1.0.0-rc.1 // indirect
|
||||
github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 // indirect
|
||||
github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59 // indirect
|
||||
github.com/blendle/zapdriver v1.3.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dfuse-io/logging v0.0.0-20210109005628-b97a57253f70 // indirect
|
||||
github.com/fatih/color v1.7.0 // indirect
|
||||
github.com/gagliardetto/treeout v0.1.4 // indirect
|
||||
github.com/go-logr/logr v1.2.0 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
|
||||
github.com/json-iterator/go v1.1.11 // indirect
|
||||
github.com/klauspost/compress v1.13.6 // indirect
|
||||
github.com/logrusorgru/aurora v2.0.3+incompatible // indirect
|
||||
github.com/mattn/go-colorable v0.0.9 // indirect
|
||||
github.com/mattn/go-isatty v0.0.3 // indirect
|
||||
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.1 // indirect
|
||||
github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1 // indirect
|
||||
github.com/mr-tron/base58 v1.2.0 // indirect
|
||||
github.com/teris-io/shortid v0.0.0-20201117134242-e59966efd125 // indirect
|
||||
github.com/tidwall/gjson v1.6.7 // indirect
|
||||
github.com/tidwall/match v1.0.3 // indirect
|
||||
github.com/tidwall/pretty v1.0.2 // indirect
|
||||
go.opencensus.io v0.22.5 // indirect
|
||||
go.uber.org/atomic v1.7.0 // indirect
|
||||
go.uber.org/multierr v1.6.0 // indirect
|
||||
go.uber.org/ratelimit v0.2.0 // indirect
|
||||
go.uber.org/zap v1.16.0 // indirect
|
||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a // indirect
|
||||
golang.org/x/sys v0.0.0-20210514084401-e8d321eab015 // indirect
|
||||
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf // indirect
|
||||
)
|
|
@ -0,0 +1,44 @@
|
|||
package endpoints
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/gagliardetto/solana-go/rpc"
|
||||
"net"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const RPCLocalhost = "http://127.0.0.1:8899"
|
||||
|
||||
func GetNodeTPUPorts(ctx context.Context, rpcHost string, nodeIP net.IP) ([]uint16, error) {
|
||||
c := rpc.New(rpcHost)
|
||||
|
||||
out, err := c.GetClusterNodes(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, node := range out {
|
||||
if node.TPU == nil {
|
||||
continue
|
||||
}
|
||||
tpuAddr := *node.TPU
|
||||
host, port, err := net.SplitHostPort(tpuAddr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing node TPU %s: %v", tpuAddr, err)
|
||||
}
|
||||
if host == nodeIP.String() {
|
||||
port, err := strconv.Atoi(port)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error parsing node TPU %s: %v", tpuAddr, err)
|
||||
}
|
||||
return []uint16{
|
||||
uint16(port), // TPU
|
||||
uint16(port + 1), // TPUfwd
|
||||
uint16(port + 2), // TPUvote
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("node %s not found in cluster", nodeIP)
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
package tpu
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"errors"
|
||||
"github.com/gagliardetto/binary"
|
||||
"github.com/gagliardetto/solana-go"
|
||||
)
|
||||
|
||||
func ParseTx(p []byte) (tx *solana.Transaction, err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = errors.New("ParseTx panic")
|
||||
}
|
||||
}()
|
||||
|
||||
tx, err = solana.TransactionFromDecoder(bin.NewBinDecoder(p))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func VerifyTxSig(tx *solana.Transaction) (ok bool) {
|
||||
msg, err := tx.Message.MarshalBinary()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
signers := ExtractSigners(tx)
|
||||
|
||||
if len(signers) != len(tx.Signatures) {
|
||||
return false
|
||||
}
|
||||
|
||||
for i, sig := range tx.Signatures {
|
||||
if !ed25519.Verify(signers[i][:], msg, sig[:]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func ExtractSigners(tx *solana.Transaction) []solana.PublicKey {
|
||||
signers := make([]solana.PublicKey, 0, len(tx.Signatures))
|
||||
for _, acc := range tx.Message.AccountKeys {
|
||||
if tx.IsSigner(acc) {
|
||||
signers = append(signers, acc)
|
||||
}
|
||||
}
|
||||
return signers
|
||||
}
|
|
@ -0,0 +1,159 @@
|
|||
package tpu
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const breakTx = `
|
||||
01cd 381f 2c7a 02e7 8131 e2dd 7973 e950
|
||||
0ee5 f641 e277 5214 2d40 a1d0 2680 7261
|
||||
373a 360b 9bb0 c1b2 0527 d05f ded6 9854
|
||||
15ce f10c 7e1e ecf7 9936 1f62 7391 a717
|
||||
0c01 0001 030b e57d dad5 f720 6443 5af8
|
||||
88f3 ea02 fb25 0872 708a ef6d 2af7 fb0d
|
||||
b3a7 fd60 902d cb74 4542 57c0 11b2 f71d
|
||||
b25c 7097 820a f6ba 23a8 581e f842 03c1
|
||||
f458 161b 81a1 3135 a853 75d0 0a62 175a
|
||||
a55b 90d5 b1b1 cd35 e740 0ff4 b648 ee90
|
||||
d1cd 6134 3b79 7e54 aae3 3c97 7e48 8f09
|
||||
2d48 a4b5 367e 88e1 6cc6 c3bb 8c66 22fa
|
||||
4491 add8 6701 0201 0102 000d
|
||||
`
|
||||
|
||||
/*
|
||||
(*solana.Transaction)(0xc0001efea0)({
|
||||
Signatures: ([]solana.Signature) (len=1 cap=1) {
|
||||
(solana.Signature) (len=64 cap=64) 64uYQJaaAbLinRXWH661uzDvdZaWFTrh8RiN7LKN18eGs2XcngX1NqRi5w8YcXzxPKodbfG1dD8XsyeNHHVhMb1g
|
||||
},
|
||||
Message: (solana.Message) {
|
||||
AccountKeys: ([]solana.PublicKey) (len=6 cap=8) {
|
||||
(solana.PublicKey) (len=32 cap=32) Gxwia5TTd63XbSMB9AhV5LtPQucGKnvjYzaUQ3iAH7GV,
|
||||
(solana.PublicKey) (len=32 cap=32) 2X122BRxKJGvjmcjQdJUouTFxKbtFLnfZWA3Uz6ST9sD,
|
||||
(solana.PublicKey) (len=32 cap=32) 4LUro5jaPaTurXK737QAxgJywdhABnFAMQkXX4ZyqqaZ,
|
||||
(solana.PublicKey) (len=32 cap=32) 9gpfTc4zsndJSdpnpXQbey16L5jW2GWcKeY3PLixqU4,
|
||||
(solana.PublicKey) (len=32 cap=32) 9xQeWvG816bUx9EPjHmaT23yvVM2ZWbrrpZb9PusVFin,
|
||||
(solana.PublicKey) (len=32 cap=32) 11111111111111111111111111111111
|
||||
},
|
||||
Header: (solana.MessageHeader) {
|
||||
NumRequiredSignatures: (uint8) 1,
|
||||
NumReadonlySignedAccounts: (uint8) 0,
|
||||
NumReadonlyUnsignedAccounts: (uint8) 2
|
||||
},
|
||||
RecentBlockhash: (solana.Hash) (len=32 cap=32) EuMLgzXp8c467FynAUwSErE4EJrbRmds6SJA5vDH2s8b,
|
||||
Instructions: ([]solana.CompiledInstruction) (len=2 cap=2) {
|
||||
(solana.CompiledInstruction) {
|
||||
ProgramIDIndex: (uint16) 4,
|
||||
AccountCount: (bin.Varuint16) 5,
|
||||
DataLength: (bin.Varuint16) 7,
|
||||
Accounts: ([]uint16) (len=5 cap=8) {
|
||||
(uint16) 1,
|
||||
(uint16) 2,
|
||||
(uint16) 3,
|
||||
(uint16) 2,
|
||||
(uint16) 2
|
||||
},
|
||||
Data: (solana.Base58) (len=7 cap=7) 12VeXEVRR
|
||||
},
|
||||
(solana.CompiledInstruction) {
|
||||
ProgramIDIndex: (uint16) 5,
|
||||
AccountCount: (bin.Varuint16) 2,
|
||||
DataLength: (bin.Varuint16) 12,
|
||||
Accounts: ([]uint16) (len=2 cap=4) {
|
||||
(uint16) 0,
|
||||
(uint16) 0
|
||||
},
|
||||
Data: (solana.Base58) (len=12 cap=12) 3Bxs43NbWyF9ibYB
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
*/
|
||||
|
||||
const tpuTx = `
|
||||
01fd 740a 299c 8140 4158 2e9e d4cc 7c96
|
||||
0c11 ead6 eefc 2681 ae9e df71 cb31 d2ca
|
||||
608b 62a1 3555 f268 82fb 622c 9c60 132c
|
||||
a362 d64d 6906 ed47 012d af73 f1b6 d805
|
||||
0f01 0002 06ed 341d e66d 6788 6d10 97e4
|
||||
ac2b cfd5 00c7 9411 872b 0f47 ed52 5786
|
||||
ce23 f750 0a16 8b21 8f1e 3359 a2be 9952
|
||||
02b4 d07f 1978 eecf e602 2fed dd83 3509
|
||||
9f9e 25af 0031 9098 8c12 0f69 b745 18e1
|
||||
5183 8682 b982 2fa5 7d3f 529d 87d2 0b05
|
||||
0ceb c02b 1a02 39ac 5042 f7fd afc3 f269
|
||||
19a7 9644 8de8 142c d2ee d20d c08e cf80
|
||||
8b79 b09d 6985 0f2d 6e02 a47a f824 d09a
|
||||
b69d c42d 70cb 28cb fa24 9fb7 ee57 b9d2
|
||||
56c1 2762 ef00 0000 0000 0000 0000 0000
|
||||
0000 0000 0000 0000 0000 0000 0000 0000
|
||||
0000 0000 00ce 9121 49be dcfc c6d4 82a1
|
||||
7a1a 6ae3 d823 35a8 57e4 d86e 2c3f b730
|
||||
e519 cca5 a802 0405 0102 0302 0207 0003
|
||||
0000 000b 0005 0200 000c 0200 0000 0f0b
|
||||
0000 0000 0000
|
||||
`
|
||||
|
||||
func parseHexdump(s string) []byte {
|
||||
s = strings.ReplaceAll(s, " ", "")
|
||||
s = strings.ReplaceAll(s, "\n", "")
|
||||
b, err := hex.DecodeString(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func TestParseTx(t *testing.T) {
|
||||
ts := []string{
|
||||
breakTx,
|
||||
tpuTx,
|
||||
}
|
||||
|
||||
for _, s := range ts {
|
||||
b := parseHexdump(s)
|
||||
_, err := ParseTx(b)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkParseTx(b *testing.B) {
|
||||
r := parseHexdump(tpuTx)
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := ParseTx(r)
|
||||
if err != nil {
|
||||
b.Error(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyTxSig(t *testing.T) {
|
||||
tx, err := ParseTx(parseHexdump(tpuTx))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if !VerifyTxSig(tx) {
|
||||
t.Error("tx verify failed")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkVerifyTxSig(b *testing.B) {
|
||||
tx, err := ParseTx(parseHexdump(tpuTx))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
VerifyTxSig(tx)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue