Release v0.14.0pull/1025/head v0.14.0
@ -1,5 +1,4 @@ | |||
# CODEOWNERS: https://help.github.com/articles/about-codeowners/ | |||
# Everything goes through Bucky. For now. | |||
* @ebuchman | |||
# Everything goes through Bucky and Anton. For now. | |||
* @ebuchman @melekes |
@ -0,0 +1,13 @@ | |||
version: 1.0.{build} | |||
configuration: Release | |||
platform: | |||
- x64 | |||
- x86 | |||
clone_folder: c:\go\path\src\github.com\tendermint\tendermint | |||
before_build: | |||
- cmd: set GOPATH=%GOROOT%\path | |||
- cmd: set PATH=%GOPATH%\bin;%PATH% | |||
- cmd: make get_vendor_deps | |||
build_script: | |||
- cmd: make test | |||
test: off |
@ -0,0 +1,60 @@ | |||
package commands | |||
import ( | |||
"github.com/spf13/cobra" | |||
cmn "github.com/tendermint/tmlibs/common" | |||
"github.com/tendermint/tendermint/lite/proxy" | |||
rpcclient "github.com/tendermint/tendermint/rpc/client" | |||
) | |||
// LiteCmd represents the base command when called without any subcommands | |||
var LiteCmd = &cobra.Command{ | |||
Use: "lite", | |||
Short: "Run lite-client proxy server, verifying tendermint rpc", | |||
Long: `This node will run a secure proxy to a tendermint rpc server. | |||
All calls that can be tracked back to a block header by a proof | |||
will be verified before passing them back to the caller. Other that | |||
that it will present the same interface as a full tendermint node, | |||
just with added trust and running locally.`, | |||
RunE: runProxy, | |||
SilenceUsage: true, | |||
} | |||
var ( | |||
listenAddr string | |||
nodeAddr string | |||
chainID string | |||
home string | |||
) | |||
func init() { | |||
LiteCmd.Flags().StringVar(&listenAddr, "laddr", ":8888", "Serve the proxy on the given port") | |||
LiteCmd.Flags().StringVar(&nodeAddr, "node", "localhost:46657", "Connect to a Tendermint node at this address") | |||
LiteCmd.Flags().StringVar(&chainID, "chain-id", "tendermint", "Specify the Tendermint chain ID") | |||
LiteCmd.Flags().StringVar(&home, "home-dir", ".tendermint-lite", "Specify the home directory") | |||
} | |||
func runProxy(cmd *cobra.Command, args []string) error { | |||
// First, connect a client | |||
node := rpcclient.NewHTTP(nodeAddr, "/websocket") | |||
cert, err := proxy.GetCertifier(chainID, home, nodeAddr) | |||
if err != nil { | |||
return err | |||
} | |||
sc := proxy.SecureClient(node, cert) | |||
err = proxy.StartProxy(sc, listenAddr, logger) | |||
if err != nil { | |||
return err | |||
} | |||
cmn.TrapSignal(func() { | |||
// TODO: close up shop | |||
}) | |||
return nil | |||
} |
@ -1,36 +0,0 @@ | |||
# Generating test data | |||
To generate the data, run `build.sh`. See that script for more details. | |||
Make sure to adjust the stepChanges in the testCases if the number of messages changes. | |||
This sometimes happens for the `small_block2.cswal`, where the number of block parts changes between 4 and 5. | |||
If you need to change the signatures, you can use a script as follows: | |||
The privBytes comes from `config/tendermint_test/...`: | |||
``` | |||
package main | |||
import ( | |||
"encoding/hex" | |||
"fmt" | |||
"github.com/tendermint/go-crypto" | |||
) | |||
func main() { | |||
signBytes, err := hex.DecodeString("7B22636861696E5F6964223A2274656E6465726D696E745F74657374222C22766F7465223A7B22626C6F636B5F68617368223A2242453544373939433846353044354645383533364334333932464443384537423342313830373638222C22626C6F636B5F70617274735F686561646572223A506172745365747B543A31204236323237323535464632307D2C22686569676874223A312C22726F756E64223A302C2274797065223A327D7D") | |||
if err != nil { | |||
panic(err) | |||
} | |||
privBytes, err := hex.DecodeString("27F82582AEFAE7AB151CFB01C48BB6C1A0DA78F9BDDA979A9F70A84D074EB07D3B3069C422E19688B45CBFAE7BB009FC0FA1B1EA86593519318B7214853803C8") | |||
if err != nil { | |||
panic(err) | |||
} | |||
privKey := crypto.PrivKeyEd25519{} | |||
copy(privKey[:], privBytes) | |||
signature := privKey.Sign(signBytes) | |||
fmt.Printf("Signature Bytes: %X\n", signature.Bytes()) | |||
} | |||
``` | |||
@ -1,148 +0,0 @@ | |||
#!/usr/bin/env bash | |||
# Requires: killall command and jq JSON processor. | |||
# Get the parent directory of where this script is. | |||
SOURCE="${BASH_SOURCE[0]}" | |||
while [ -h "$SOURCE" ] ; do SOURCE="$(readlink "$SOURCE")"; done | |||
DIR="$( cd -P "$( dirname "$SOURCE" )/../.." && pwd )" | |||
# Change into that dir because we expect that. | |||
cd "$DIR" || exit 1 | |||
# Make sure we have a tendermint command. | |||
if ! hash tendermint 2>/dev/null; then | |||
make install | |||
fi | |||
# Make sure we have a cutWALUntil binary. | |||
cutWALUntil=./scripts/cutWALUntil/cutWALUntil | |||
cutWALUntilDir=$(dirname $cutWALUntil) | |||
if ! hash $cutWALUntil 2>/dev/null; then | |||
cd "$cutWALUntilDir" && go build && cd - || exit 1 | |||
fi | |||
TMHOME=$(mktemp -d) | |||
export TMHOME="$TMHOME" | |||
if [[ ! -d "$TMHOME" ]]; then | |||
echo "Could not create temp directory" | |||
exit 1 | |||
else | |||
echo "TMHOME: ${TMHOME}" | |||
fi | |||
# TODO: eventually we should replace with `tendermint init --test` | |||
DIR_TO_COPY=$HOME/.tendermint_test/consensus_state_test | |||
if [ ! -d "$DIR_TO_COPY" ]; then | |||
echo "$DIR_TO_COPY does not exist. Please run: go test ./consensus" | |||
exit 1 | |||
fi | |||
echo "==> Copying ${DIR_TO_COPY} to ${TMHOME} directory..." | |||
cp -r "$DIR_TO_COPY"/* "$TMHOME" | |||
# preserve original genesis file because later it will be modified (see small_block2) | |||
cp "$TMHOME/genesis.json" "$TMHOME/genesis.json.bak" | |||
function reset(){ | |||
echo "==> Resetting tendermint..." | |||
tendermint unsafe_reset_all | |||
cp "$TMHOME/genesis.json.bak" "$TMHOME/genesis.json" | |||
} | |||
reset | |||
# function empty_block(){ | |||
# echo "==> Starting tendermint..." | |||
# tendermint node --proxy_app=persistent_dummy &> /dev/null & | |||
# sleep 5 | |||
# echo "==> Killing tendermint..." | |||
# killall tendermint | |||
# echo "==> Copying WAL log..." | |||
# $cutWALUntil "$TMHOME/data/cs.wal/wal" 1 consensus/test_data/new_empty_block.cswal | |||
# mv consensus/test_data/new_empty_block.cswal consensus/test_data/empty_block.cswal | |||
# reset | |||
# } | |||
function many_blocks(){ | |||
bash scripts/txs/random.sh 1000 36657 &> /dev/null & | |||
PID=$! | |||
echo "==> Starting tendermint..." | |||
tendermint node --proxy_app=persistent_dummy &> /dev/null & | |||
sleep 10 | |||
echo "==> Killing tendermint..." | |||
kill -9 $PID | |||
killall tendermint | |||
echo "==> Copying WAL log..." | |||
$cutWALUntil "$TMHOME/data/cs.wal/wal" 6 consensus/test_data/new_many_blocks.cswal | |||
mv consensus/test_data/new_many_blocks.cswal consensus/test_data/many_blocks.cswal | |||
reset | |||
} | |||
# function small_block1(){ | |||
# bash scripts/txs/random.sh 1000 36657 &> /dev/null & | |||
# PID=$! | |||
# echo "==> Starting tendermint..." | |||
# tendermint node --proxy_app=persistent_dummy &> /dev/null & | |||
# sleep 10 | |||
# echo "==> Killing tendermint..." | |||
# kill -9 $PID | |||
# killall tendermint | |||
# echo "==> Copying WAL log..." | |||
# $cutWALUntil "$TMHOME/data/cs.wal/wal" 1 consensus/test_data/new_small_block1.cswal | |||
# mv consensus/test_data/new_small_block1.cswal consensus/test_data/small_block1.cswal | |||
# reset | |||
# } | |||
# # block part size = 512 | |||
# function small_block2(){ | |||
# cat "$TMHOME/genesis.json" | jq '. + {consensus_params: {block_size_params: {max_bytes: 22020096}, block_gossip_params: {block_part_size_bytes: 512}}}' > "$TMHOME/new_genesis.json" | |||
# mv "$TMHOME/new_genesis.json" "$TMHOME/genesis.json" | |||
# bash scripts/txs/random.sh 1000 36657 &> /dev/null & | |||
# PID=$! | |||
# echo "==> Starting tendermint..." | |||
# tendermint node --proxy_app=persistent_dummy &> /dev/null & | |||
# sleep 5 | |||
# echo "==> Killing tendermint..." | |||
# kill -9 $PID | |||
# killall tendermint | |||
# echo "==> Copying WAL log..." | |||
# $cutWALUntil "$TMHOME/data/cs.wal/wal" 1 consensus/test_data/new_small_block2.cswal | |||
# mv consensus/test_data/new_small_block2.cswal consensus/test_data/small_block2.cswal | |||
# reset | |||
# } | |||
case "$1" in | |||
# "small_block1") | |||
# small_block1 | |||
# ;; | |||
# "small_block2") | |||
# small_block2 | |||
# ;; | |||
# "empty_block") | |||
# empty_block | |||
# ;; | |||
"many_blocks") | |||
many_blocks | |||
;; | |||
*) | |||
# small_block1 | |||
# small_block2 | |||
# empty_block | |||
many_blocks | |||
esac | |||
echo "==> Cleaning up..." | |||
rm -rf "$TMHOME" |
@ -0,0 +1,181 @@ | |||
package consensus | |||
import ( | |||
"bufio" | |||
"bytes" | |||
"fmt" | |||
"math/rand" | |||
"os" | |||
"path/filepath" | |||
"strings" | |||
"time" | |||
"github.com/pkg/errors" | |||
"github.com/tendermint/abci/example/dummy" | |||
bc "github.com/tendermint/tendermint/blockchain" | |||
cfg "github.com/tendermint/tendermint/config" | |||
"github.com/tendermint/tendermint/proxy" | |||
sm "github.com/tendermint/tendermint/state" | |||
"github.com/tendermint/tendermint/types" | |||
auto "github.com/tendermint/tmlibs/autofile" | |||
"github.com/tendermint/tmlibs/db" | |||
"github.com/tendermint/tmlibs/log" | |||
) | |||
// WALWithNBlocks generates a consensus WAL. It does this by spining up a | |||
// stripped down version of node (proxy app, event bus, consensus state) with a | |||
// persistent dummy application and special consensus wal instance | |||
// (byteBufferWAL) and waits until numBlocks are created. Then it returns a WAL | |||
// content. | |||
func WALWithNBlocks(numBlocks int) (data []byte, err error) { | |||
config := getConfig() | |||
app := dummy.NewPersistentDummyApplication(filepath.Join(config.DBDir(), "wal_generator")) | |||
logger := log.NewNopLogger() // log.TestingLogger().With("wal_generator", "wal_generator") | |||
///////////////////////////////////////////////////////////////////////////// | |||
// COPY PASTE FROM node.go WITH A FEW MODIFICATIONS | |||
// NOTE: we can't import node package because of circular dependency | |||
privValidatorFile := config.PrivValidatorFile() | |||
privValidator := types.LoadOrGenPrivValidatorFS(privValidatorFile) | |||
genDoc, err := types.GenesisDocFromFile(config.GenesisFile()) | |||
if err != nil { | |||
return nil, errors.Wrap(err, "failed to read genesis file") | |||
} | |||
stateDB := db.NewMemDB() | |||
blockStoreDB := db.NewMemDB() | |||
state, err := sm.MakeGenesisState(stateDB, genDoc) | |||
state.SetLogger(logger.With("module", "state")) | |||
if err != nil { | |||
return nil, errors.Wrap(err, "failed to make genesis state") | |||
} | |||
blockStore := bc.NewBlockStore(blockStoreDB) | |||
handshaker := NewHandshaker(state, blockStore) | |||
proxyApp := proxy.NewAppConns(proxy.NewLocalClientCreator(app), handshaker) | |||
proxyApp.SetLogger(logger.With("module", "proxy")) | |||
if err := proxyApp.Start(); err != nil { | |||
return nil, errors.Wrap(err, "failed to start proxy app connections") | |||
} | |||
defer proxyApp.Stop() | |||
eventBus := types.NewEventBus() | |||
eventBus.SetLogger(logger.With("module", "events")) | |||
if err := eventBus.Start(); err != nil { | |||
return nil, errors.Wrap(err, "failed to start event bus") | |||
} | |||
mempool := types.MockMempool{} | |||
consensusState := NewConsensusState(config.Consensus, state.Copy(), proxyApp.Consensus(), blockStore, mempool) | |||
consensusState.SetLogger(logger) | |||
consensusState.SetEventBus(eventBus) | |||
if privValidator != nil { | |||
consensusState.SetPrivValidator(privValidator) | |||
} | |||
// END OF COPY PASTE | |||
///////////////////////////////////////////////////////////////////////////// | |||
// set consensus wal to buffered WAL, which will write all incoming msgs to buffer | |||
var b bytes.Buffer | |||
wr := bufio.NewWriter(&b) | |||
numBlocksWritten := make(chan struct{}) | |||
wal := &byteBufferWAL{enc: NewWALEncoder(wr), heightToStop: int64(numBlocks), signalWhenStopsTo: numBlocksWritten} | |||
// see wal.go#103 | |||
wal.Save(EndHeightMessage{0}) | |||
consensusState.wal = wal | |||
if err := consensusState.Start(); err != nil { | |||
return nil, errors.Wrap(err, "failed to start consensus state") | |||
} | |||
defer consensusState.Stop() | |||
select { | |||
case <-numBlocksWritten: | |||
wr.Flush() | |||
return b.Bytes(), nil | |||
case <-time.After(time.Duration(5*numBlocks) * time.Second): | |||
return b.Bytes(), fmt.Errorf("waited too long for tendermint to produce %d blocks", numBlocks) | |||
} | |||
} | |||
// f**ing long, but unique for each test | |||
func makePathname() string { | |||
// get path | |||
p, err := os.Getwd() | |||
if err != nil { | |||
panic(err) | |||
} | |||
// fmt.Println(p) | |||
sep := string(filepath.Separator) | |||
return strings.Replace(p, sep, "_", -1) | |||
} | |||
func randPort() int { | |||
// returns between base and base + spread | |||
base, spread := 20000, 20000 | |||
return base + rand.Intn(spread) | |||
} | |||
func makeAddrs() (string, string, string) { | |||
start := randPort() | |||
return fmt.Sprintf("tcp://0.0.0.0:%d", start), | |||
fmt.Sprintf("tcp://0.0.0.0:%d", start+1), | |||
fmt.Sprintf("tcp://0.0.0.0:%d", start+2) | |||
} | |||
// getConfig returns a config for test cases | |||
func getConfig() *cfg.Config { | |||
pathname := makePathname() | |||
c := cfg.ResetTestRoot(pathname) | |||
// and we use random ports to run in parallel | |||
tm, rpc, grpc := makeAddrs() | |||
c.P2P.ListenAddress = tm | |||
c.RPC.ListenAddress = rpc | |||
c.RPC.GRPCListenAddress = grpc | |||
return c | |||
} | |||
// byteBufferWAL is a WAL which writes all msgs to a byte buffer. Writing stops | |||
// when the heightToStop is reached. Client will be notified via | |||
// signalWhenStopsTo channel. | |||
type byteBufferWAL struct { | |||
enc *WALEncoder | |||
stopped bool | |||
heightToStop int64 | |||
signalWhenStopsTo chan struct{} | |||
} | |||
// needed for determinism | |||
var fixedTime, _ = time.Parse(time.RFC3339, "2017-01-02T15:04:05Z") | |||
// Save writes message to the internal buffer except when heightToStop is | |||
// reached, in which case it will signal the caller via signalWhenStopsTo and | |||
// skip writing. | |||
func (w *byteBufferWAL) Save(m WALMessage) { | |||
if w.stopped { | |||
return | |||
} | |||
if endMsg, ok := m.(EndHeightMessage); ok { | |||
if endMsg.Height == w.heightToStop { | |||
w.signalWhenStopsTo <- struct{}{} | |||
w.stopped = true | |||
return | |||
} | |||
} | |||
err := w.enc.Encode(&TimedWALMessage{fixedTime, m}) | |||
if err != nil { | |||
panic(fmt.Sprintf("failed to encode the msg %v", m)) | |||
} | |||
} | |||
func (w *byteBufferWAL) Group() *auto.Group { | |||
panic("not implemented") | |||
} | |||
func (w *byteBufferWAL) SearchForEndHeight(height int64) (gr *auto.GroupReader, found bool, err error) { | |||
return nil, false, nil | |||
} | |||
func (w *byteBufferWAL) Start() error { return nil } | |||
func (w *byteBufferWAL) Stop() error { return nil } | |||
func (w *byteBufferWAL) Wait() {} |
@ -0,0 +1,103 @@ | |||
# ADR 007: Trust Metric Usage Guide | |||
## Context | |||
Tendermint is required to monitor peer quality in order to inform its peer dialing and peer exchange strategies. | |||
When a node first connects to the network, it is important that it can quickly find good peers. | |||
Thus, while a node has fewer connections, it should prioritize connecting to higher quality peers. | |||
As the node becomes well connected to the rest of the network, it can dial lesser known or lesser | |||
quality peers and help assess their quality. Similarly, when queried for peers, a node should make | |||
sure they dont return low quality peers. | |||
Peer quality can be tracked using a trust metric that flags certain behaviours as good or bad. When enough | |||
bad behaviour accumulates, we can mark the peer as bad and disconnect. | |||
For example, when the PEXReactor makes a request for peers network addresses from an already known peer, and the returned network addresses are unreachable, this undesirable behavior should be tracked. Returning a few bad network addresses probably shouldn’t cause a peer to be dropped, while excessive amounts of this behavior does qualify the peer for removal. The originally proposed approach and design document for the trust metric can be found in the [ADR 006](adr-006-trust-metric.md) document. | |||
The trust metric implementation allows a developer to obtain a peer's trust metric from a trust metric store, and track good and bad events relevant to a peer's behavior, and at any time, the peer's metric can be queried for a current trust value. The current trust value is calculated with a formula that utilizes current behavior, previous behavior, and change between the two. Current behavior is calculated as the percentage of good behavior within a time interval. The time interval is short; probably set between 30 seconds and 5 minutes. On the other hand, the historic data can estimate a peer's behavior over days worth of tracking. At the end of a time interval, the current behavior becomes part of the historic data, and a new time interval begins with the good and bad counters reset to zero. | |||
These are some important things to keep in mind regarding how the trust metrics handle time intervals and scoring: | |||
- Each new time interval begins with a perfect score | |||
- Bad events quickly bring the score down and good events cause the score to slowly rise | |||
- When the time interval is over, the percentage of good events becomes historic data. | |||
Some useful information about the inner workings of the trust metric: | |||
- When a trust metric is first instantiated, a timer (ticker) periodically fires in order to handle transitions between trust metric time intervals | |||
- If a peer is disconnected from a node, the timer should be paused, since the node is no longer connected to that peer | |||
- The ability to pause the metric is supported with the store **PeerDisconnected** method and the metric **Pause** method | |||
- After a pause, if a good or bad event method is called on a metric, it automatically becomes unpaused and begins a new time interval. | |||
## Decision | |||
The trust metric capability is now available, yet, it still leaves the question of how should it be applied throughout Tendermint in order to properly track the quality of peers? | |||
### Proposed Process | |||
Peers are managed using an address book and a trust metric: | |||
- The address book keeps a record of peers and provides selection methods | |||
- The trust metric tracks the quality of the peers | |||
#### Presence in Address Book | |||
Outbound peers are added to the address book before they are dialed, | |||
and inbound peers are added once the peer connection is set up. | |||
Peers are also added to the address book when they are received in response to | |||
a pexRequestMessage. | |||
While a node has less than `needAddressThreshold`, it will periodically request more, | |||
via pexRequestMessage, from randomly selected peers and from newly dialed outbound peers. | |||
When a new address is added to an address book that has more than `0.5*needAddressThreshold` addresses, | |||
then with some low probability, a randomly chosen low quality peer is removed. | |||
#### Outbound Peers | |||
Peers attempt to maintain a minimum number of outbound connections by | |||
repeatedly querying the address book for peers to connect to. | |||
While a node has few to no outbound connections, the address book is biased to return | |||
higher quality peers. As the node increases the number of outbound connections, | |||
the address book is biased to return less-vetted or lower-quality peers. | |||
#### Inbound Peers | |||
Peers also maintain a maximum number of total connections, MaxNumPeers. | |||
If a peer has MaxNumPeers, new incoming connections will be accepted with low probability. | |||
When such a new connection is accepted, the peer disconnects from a probabilistically chosen low ranking peer | |||
so it does not exceed MaxNumPeers. | |||
#### Peer Exchange | |||
When a peer receives a pexRequestMessage, it returns a random sample of high quality peers from the address book. Peers with no score or low score should not be inclided in a response to pexRequestMessage. | |||
#### Peer Quality | |||
Peer quality is tracked in the connection and across the reactors by storing the TrustMetric in the peer's | |||
thread safe Data store. | |||
Peer behaviour is then defined as one of the following: | |||
- Fatal - something outright malicious that causes us to disconnect the peer and ban it from the address book for some amount of time | |||
- Bad - Any kind of timeout, messages that don't unmarshal, fail other validity checks, or messages we didn't ask for or aren't expecting (usually worth one bad event) | |||
- Neutral - Unknown channels/message types/version upgrades (no good or bad events recorded) | |||
- Correct - Normal correct behavior (worth one good event) | |||
- Good - some random majority of peers per reactor sending us useful messages (worth more than one good event). | |||
Note that Fatal behaviour causes us to remove the peer, and neutral behaviour does not affect the score. | |||
## Status | |||
Proposed. | |||
## Consequences | |||
### Positive | |||
- Bringing the address book and trust metric store together will cause the network to be built in a way that encourages greater security and reliability. | |||
### Negative | |||
- TBD | |||
### Neutral | |||
- Keep in mind that, good events need to be recorded just as bad events do using this implementation. |
@ -0,0 +1,40 @@ | |||
package proxy | |||
import ( | |||
"bytes" | |||
"github.com/pkg/errors" | |||
"github.com/tendermint/tendermint/lite" | |||
certerr "github.com/tendermint/tendermint/lite/errors" | |||
"github.com/tendermint/tendermint/types" | |||
) | |||
func ValidateBlockMeta(meta *types.BlockMeta, check lite.Commit) error { | |||
// TODO: check the BlockID?? | |||
return ValidateHeader(meta.Header, check) | |||
} | |||
func ValidateBlock(meta *types.Block, check lite.Commit) error { | |||
err := ValidateHeader(meta.Header, check) | |||
if err != nil { | |||
return err | |||
} | |||
if !bytes.Equal(meta.Data.Hash(), meta.Header.DataHash) { | |||
return errors.New("Data hash doesn't match header") | |||
} | |||
return nil | |||
} | |||
func ValidateHeader(head *types.Header, check lite.Commit) error { | |||
// make sure they are for the same height (obvious fail) | |||
if head.Height != check.Height() { | |||
return certerr.ErrHeightMismatch(head.Height, check.Height()) | |||
} | |||
// check if they are equal by using hashes | |||
chead := check.Header | |||
if !bytes.Equal(head.Hash(), chead.Hash()) { | |||
return errors.New("Headers don't match") | |||
} | |||
return nil | |||
} |
@ -0,0 +1,30 @@ | |||
package proxy | |||
import ( | |||
"github.com/tendermint/tendermint/lite" | |||
certclient "github.com/tendermint/tendermint/lite/client" | |||
"github.com/tendermint/tendermint/lite/files" | |||
) | |||
func GetCertifier(chainID, rootDir, nodeAddr string) (*lite.Inquiring, error) { | |||
trust := lite.NewCacheProvider( | |||
lite.NewMemStoreProvider(), | |||
files.NewProvider(rootDir), | |||
) | |||
source := certclient.NewHTTPProvider(nodeAddr) | |||
// XXX: total insecure hack to avoid `init` | |||
fc, err := source.LatestCommit() | |||
/* XXX | |||
// this gets the most recent verified commit | |||
fc, err := trust.LatestCommit() | |||
if certerr.IsCommitNotFoundErr(err) { | |||
return nil, errors.New("Please run init first to establish a root of trust") | |||
}*/ | |||
if err != nil { | |||
return nil, err | |||
} | |||
cert := lite.NewInquiring(chainID, fc, trust, source) | |||
return cert, nil | |||
} |
@ -0,0 +1,22 @@ | |||
package proxy | |||
import ( | |||
"fmt" | |||
"github.com/pkg/errors" | |||
) | |||
//-------------------------------------------- | |||
var errNoData = fmt.Errorf("No data returned for query") | |||
// IsNoDataErr checks whether an error is due to a query returning empty data | |||
func IsNoDataErr(err error) bool { | |||
return errors.Cause(err) == errNoData | |||
} | |||
func ErrNoData() error { | |||
return errors.WithStack(errNoData) | |||
} | |||
//-------------------------------------------- |
@ -0,0 +1,17 @@ | |||
package proxy | |||
import ( | |||
"errors" | |||
"testing" | |||
"github.com/stretchr/testify/assert" | |||
) | |||
func TestErrorNoData(t *testing.T) { | |||
e1 := ErrNoData() | |||
assert.True(t, IsNoDataErr(e1)) | |||
e2 := errors.New("foobar") | |||
assert.False(t, IsNoDataErr(e2)) | |||
assert.False(t, IsNoDataErr(nil)) | |||
} |
@ -0,0 +1,69 @@ | |||
package proxy | |||
import ( | |||
"net/http" | |||
"github.com/tendermint/tmlibs/log" | |||
rpcclient "github.com/tendermint/tendermint/rpc/client" | |||
"github.com/tendermint/tendermint/rpc/core" | |||
rpc "github.com/tendermint/tendermint/rpc/lib/server" | |||
) | |||
const ( | |||
wsEndpoint = "/websocket" | |||
) | |||
// StartProxy will start the websocket manager on the client, | |||
// set up the rpc routes to proxy via the given client, | |||
// and start up an http/rpc server on the location given by bind (eg. :1234) | |||
func StartProxy(c rpcclient.Client, listenAddr string, logger log.Logger) error { | |||
c.Start() | |||
r := RPCRoutes(c) | |||
// build the handler... | |||
mux := http.NewServeMux() | |||
rpc.RegisterRPCFuncs(mux, r, logger) | |||
wm := rpc.NewWebsocketManager(r, rpc.EventSubscriber(c)) | |||
wm.SetLogger(logger) | |||
core.SetLogger(logger) | |||
mux.HandleFunc(wsEndpoint, wm.WebsocketHandler) | |||
_, err := rpc.StartHTTPServer(listenAddr, mux, logger) | |||
return err | |||
} | |||
// RPCRoutes just routes everything to the given client, as if it were | |||
// a tendermint fullnode. | |||
// | |||
// if we want security, the client must implement it as a secure client | |||
func RPCRoutes(c rpcclient.Client) map[string]*rpc.RPCFunc { | |||
return map[string]*rpc.RPCFunc{ | |||
// Subscribe/unsubscribe are reserved for websocket events. | |||
// We can just use the core tendermint impl, which uses the | |||
// EventSwitch we registered in NewWebsocketManager above | |||
"subscribe": rpc.NewWSRPCFunc(core.Subscribe, "query"), | |||
"unsubscribe": rpc.NewWSRPCFunc(core.Unsubscribe, "query"), | |||
// info API | |||
"status": rpc.NewRPCFunc(c.Status, ""), | |||
"blockchain": rpc.NewRPCFunc(c.BlockchainInfo, "minHeight,maxHeight"), | |||
"genesis": rpc.NewRPCFunc(c.Genesis, ""), | |||
"block": rpc.NewRPCFunc(c.Block, "height"), | |||
"commit": rpc.NewRPCFunc(c.Commit, "height"), | |||
"tx": rpc.NewRPCFunc(c.Tx, "hash,prove"), | |||
"validators": rpc.NewRPCFunc(c.Validators, ""), | |||
// broadcast API | |||
"broadcast_tx_commit": rpc.NewRPCFunc(c.BroadcastTxCommit, "tx"), | |||
"broadcast_tx_sync": rpc.NewRPCFunc(c.BroadcastTxSync, "tx"), | |||
"broadcast_tx_async": rpc.NewRPCFunc(c.BroadcastTxAsync, "tx"), | |||
// abci API | |||
"abci_query": rpc.NewRPCFunc(c.ABCIQuery, "path,data,prove"), | |||
"abci_info": rpc.NewRPCFunc(c.ABCIInfo, ""), | |||
} | |||
} |
@ -0,0 +1,120 @@ | |||
package proxy | |||
import ( | |||
"github.com/pkg/errors" | |||
"github.com/tendermint/go-wire/data" | |||
"github.com/tendermint/iavl" | |||
"github.com/tendermint/tendermint/lite" | |||
"github.com/tendermint/tendermint/lite/client" | |||
certerr "github.com/tendermint/tendermint/lite/errors" | |||
rpcclient "github.com/tendermint/tendermint/rpc/client" | |||
ctypes "github.com/tendermint/tendermint/rpc/core/types" | |||
) | |||
// GetWithProof will query the key on the given node, and verify it has | |||
// a valid proof, as defined by the certifier. | |||
// | |||
// If there is any error in checking, returns an error. | |||
// If val is non-empty, proof should be KeyExistsProof | |||
// If val is empty, proof should be KeyMissingProof | |||
func GetWithProof(key []byte, reqHeight int64, node rpcclient.Client, | |||
cert lite.Certifier) ( | |||
val data.Bytes, height int64, proof iavl.KeyProof, err error) { | |||
if reqHeight < 0 { | |||
err = errors.Errorf("Height cannot be negative") | |||
return | |||
} | |||
_resp, proof, err := GetWithProofOptions("/key", key, | |||
rpcclient.ABCIQueryOptions{Height: int64(reqHeight)}, | |||
node, cert) | |||
if _resp != nil { | |||
resp := _resp.Response | |||
val, height = resp.Value, resp.Height | |||
} | |||
return val, height, proof, err | |||
} | |||
// GetWithProofOptions is useful if you want full access to the ABCIQueryOptions | |||
func GetWithProofOptions(path string, key []byte, opts rpcclient.ABCIQueryOptions, | |||
node rpcclient.Client, cert lite.Certifier) ( | |||
*ctypes.ResultABCIQuery, iavl.KeyProof, error) { | |||
_resp, err := node.ABCIQueryWithOptions(path, key, opts) | |||
if err != nil { | |||
return nil, nil, err | |||
} | |||
resp := _resp.Response | |||
// make sure the proof is the proper height | |||
if resp.IsErr() { | |||
err = errors.Errorf("Query error %d: %d", resp.Code) | |||
return nil, nil, err | |||
} | |||
if len(resp.Key) == 0 || len(resp.Proof) == 0 { | |||
return nil, nil, ErrNoData() | |||
} | |||
if resp.Height == 0 { | |||
return nil, nil, errors.New("Height returned is zero") | |||
} | |||
// AppHash for height H is in header H+1 | |||
commit, err := GetCertifiedCommit(resp.Height+1, node, cert) | |||
if err != nil { | |||
return nil, nil, err | |||
} | |||
if len(resp.Value) > 0 { | |||
// The key was found, construct a proof of existence. | |||
eproof, err := iavl.ReadKeyExistsProof(resp.Proof) | |||
if err != nil { | |||
return nil, nil, errors.Wrap(err, "Error reading proof") | |||
} | |||
// Validate the proof against the certified header to ensure data integrity. | |||
err = eproof.Verify(resp.Key, resp.Value, commit.Header.AppHash) | |||
if err != nil { | |||
return nil, nil, errors.Wrap(err, "Couldn't verify proof") | |||
} | |||
return &ctypes.ResultABCIQuery{resp}, eproof, nil | |||
} | |||
// The key wasn't found, construct a proof of non-existence. | |||
var aproof *iavl.KeyAbsentProof | |||
aproof, err = iavl.ReadKeyAbsentProof(resp.Proof) | |||
if err != nil { | |||
return nil, nil, errors.Wrap(err, "Error reading proof") | |||
} | |||
// Validate the proof against the certified header to ensure data integrity. | |||
err = aproof.Verify(resp.Key, nil, commit.Header.AppHash) | |||
if err != nil { | |||
return nil, nil, errors.Wrap(err, "Couldn't verify proof") | |||
} | |||
return &ctypes.ResultABCIQuery{resp}, aproof, ErrNoData() | |||
} | |||
// GetCertifiedCommit gets the signed header for a given height | |||
// and certifies it. Returns error if unable to get a proven header. | |||
func GetCertifiedCommit(h int64, node rpcclient.Client, | |||
cert lite.Certifier) (empty lite.Commit, err error) { | |||
// FIXME: cannot use cert.GetByHeight for now, as it also requires | |||
// Validators and will fail on querying tendermint for non-current height. | |||
// When this is supported, we should use it instead... | |||
rpcclient.WaitForHeight(node, h, nil) | |||
cresp, err := node.Commit(&h) | |||
if err != nil { | |||
return | |||
} | |||
commit := client.CommitFromResult(cresp) | |||
// validate downloaded checkpoint with our request and trust store. | |||
if commit.Height() != h { | |||
return empty, certerr.ErrHeightMismatch(h, commit.Height()) | |||
} | |||
err = cert.Certify(commit) | |||
return commit, nil | |||
} |
@ -0,0 +1,140 @@ | |||
package proxy | |||
import ( | |||
"fmt" | |||
"os" | |||
"testing" | |||
"github.com/stretchr/testify/assert" | |||
"github.com/stretchr/testify/require" | |||
"github.com/tendermint/abci/example/dummy" | |||
"github.com/tendermint/tendermint/lite" | |||
certclient "github.com/tendermint/tendermint/lite/client" | |||
nm "github.com/tendermint/tendermint/node" | |||
"github.com/tendermint/tendermint/rpc/client" | |||
rpctest "github.com/tendermint/tendermint/rpc/test" | |||
"github.com/tendermint/tendermint/types" | |||
) | |||
var node *nm.Node | |||
// TODO fix tests!! | |||
func TestMain(m *testing.M) { | |||
app := dummy.NewDummyApplication() | |||
node = rpctest.StartTendermint(app) | |||
code := m.Run() | |||
node.Stop() | |||
node.Wait() | |||
os.Exit(code) | |||
} | |||
func dummyTx(k, v []byte) []byte { | |||
return []byte(fmt.Sprintf("%s=%s", k, v)) | |||
} | |||
func _TestAppProofs(t *testing.T) { | |||
assert, require := assert.New(t), require.New(t) | |||
cl := client.NewLocal(node) | |||
client.WaitForHeight(cl, 1, nil) | |||
k := []byte("my-key") | |||
v := []byte("my-value") | |||
tx := dummyTx(k, v) | |||
br, err := cl.BroadcastTxCommit(tx) | |||
require.NoError(err, "%+v", err) | |||
require.EqualValues(0, br.CheckTx.Code, "%#v", br.CheckTx) | |||
require.EqualValues(0, br.DeliverTx.Code) | |||
brh := br.Height | |||
// This sets up our trust on the node based on some past point. | |||
source := certclient.NewProvider(cl) | |||
seed, err := source.GetByHeight(brh - 2) | |||
require.NoError(err, "%+v", err) | |||
cert := lite.NewStatic("my-chain", seed.Validators) | |||
client.WaitForHeight(cl, 3, nil) | |||
latest, err := source.LatestCommit() | |||
require.NoError(err, "%+v", err) | |||
rootHash := latest.Header.AppHash | |||
// verify a query before the tx block has no data (and valid non-exist proof) | |||
bs, height, proof, err := GetWithProof(k, brh-1, cl, cert) | |||
fmt.Println(bs, height, proof, err) | |||
require.NotNil(err) | |||
require.True(IsNoDataErr(err), err.Error()) | |||
require.Nil(bs) | |||
// but given that block it is good | |||
bs, height, proof, err = GetWithProof(k, brh, cl, cert) | |||
require.NoError(err, "%+v", err) | |||
require.NotNil(proof) | |||
require.True(height >= int64(latest.Header.Height)) | |||
// Alexis there is a bug here, somehow the above code gives us rootHash = nil | |||
// and proof.Verify doesn't care, while proofNotExists.Verify fails. | |||
// I am hacking this in to make it pass, but please investigate further. | |||
rootHash = proof.Root() | |||
//err = wire.ReadBinaryBytes(bs, &data) | |||
//require.NoError(err, "%+v", err) | |||
assert.EqualValues(v, bs) | |||
err = proof.Verify(k, bs, rootHash) | |||
assert.NoError(err, "%+v", err) | |||
// Test non-existing key. | |||
missing := []byte("my-missing-key") | |||
bs, _, proof, err = GetWithProof(missing, 0, cl, cert) | |||
require.True(IsNoDataErr(err)) | |||
require.Nil(bs) | |||
require.NotNil(proof) | |||
err = proof.Verify(missing, nil, rootHash) | |||
assert.NoError(err, "%+v", err) | |||
err = proof.Verify(k, nil, rootHash) | |||
assert.Error(err) | |||
} | |||
func _TestTxProofs(t *testing.T) { | |||
assert, require := assert.New(t), require.New(t) | |||
cl := client.NewLocal(node) | |||
client.WaitForHeight(cl, 1, nil) | |||
tx := dummyTx([]byte("key-a"), []byte("value-a")) | |||
br, err := cl.BroadcastTxCommit(tx) | |||
require.NoError(err, "%+v", err) | |||
require.EqualValues(0, br.CheckTx.Code, "%#v", br.CheckTx) | |||
require.EqualValues(0, br.DeliverTx.Code) | |||
brh := br.Height | |||
source := certclient.NewProvider(cl) | |||
seed, err := source.GetByHeight(brh - 2) | |||
require.NoError(err, "%+v", err) | |||
cert := lite.NewStatic("my-chain", seed.Validators) | |||
// First let's make sure a bogus transaction hash returns a valid non-existence proof. | |||
key := types.Tx([]byte("bogus")).Hash() | |||
res, err := cl.Tx(key, true) | |||
require.NotNil(err) | |||
require.Contains(err.Error(), "not found") | |||
// Now let's check with the real tx hash. | |||
key = types.Tx(tx).Hash() | |||
res, err = cl.Tx(key, true) | |||
require.NoError(err, "%+v", err) | |||
require.NotNil(res) | |||
err = res.Proof.Validate(key) | |||
assert.NoError(err, "%+v", err) | |||
commit, err := GetCertifiedCommit(br.Height, cl, cert) | |||
require.Nil(err, "%+v", err) | |||
require.Equal(res.Proof.RootHash, commit.Header.DataHash) | |||
} |
@ -0,0 +1,185 @@ | |||
package proxy | |||
import ( | |||
"github.com/tendermint/go-wire/data" | |||
"github.com/tendermint/tendermint/lite" | |||
certclient "github.com/tendermint/tendermint/lite/client" | |||
rpcclient "github.com/tendermint/tendermint/rpc/client" | |||
ctypes "github.com/tendermint/tendermint/rpc/core/types" | |||
) | |||
var _ rpcclient.Client = Wrapper{} | |||
// Wrapper wraps a rpcclient with a Certifier and double-checks any input that is | |||
// provable before passing it along. Allows you to make any rpcclient fully secure. | |||
type Wrapper struct { | |||
rpcclient.Client | |||
cert *lite.Inquiring | |||
} | |||
// SecureClient uses a given certifier to wrap an connection to an untrusted | |||
// host and return a cryptographically secure rpc client. | |||
// | |||
// If it is wrapping an HTTP rpcclient, it will also wrap the websocket interface | |||
func SecureClient(c rpcclient.Client, cert *lite.Inquiring) Wrapper { | |||
wrap := Wrapper{c, cert} | |||
// TODO: no longer possible as no more such interface exposed.... | |||
// if we wrap http client, then we can swap out the event switch to filter | |||
// if hc, ok := c.(*rpcclient.HTTP); ok { | |||
// evt := hc.WSEvents.EventSwitch | |||
// hc.WSEvents.EventSwitch = WrappedSwitch{evt, wrap} | |||
// } | |||
return wrap | |||
} | |||
// ABCIQueryWithOptions exposes all options for the ABCI query and verifies the returned proof | |||
func (w Wrapper) ABCIQueryWithOptions(path string, data data.Bytes, opts rpcclient.ABCIQueryOptions) (*ctypes.ResultABCIQuery, error) { | |||
res, _, err := GetWithProofOptions(path, data, opts, w.Client, w.cert) | |||
return res, err | |||
} | |||
// ABCIQuery uses default options for the ABCI query and verifies the returned proof | |||
func (w Wrapper) ABCIQuery(path string, data data.Bytes) (*ctypes.ResultABCIQuery, error) { | |||
return w.ABCIQueryWithOptions(path, data, rpcclient.DefaultABCIQueryOptions) | |||
} | |||
// Tx queries for a given tx and verifies the proof if it was requested | |||
func (w Wrapper) Tx(hash []byte, prove bool) (*ctypes.ResultTx, error) { | |||
res, err := w.Client.Tx(hash, prove) | |||
if !prove || err != nil { | |||
return res, err | |||
} | |||
h := int64(res.Height) | |||
check, err := GetCertifiedCommit(h, w.Client, w.cert) | |||
if err != nil { | |||
return res, err | |||
} | |||
err = res.Proof.Validate(check.Header.DataHash) | |||
return res, err | |||
} | |||
// BlockchainInfo requests a list of headers and verifies them all... | |||
// Rather expensive. | |||
// | |||
// TODO: optimize this if used for anything needing performance | |||
func (w Wrapper) BlockchainInfo(minHeight, maxHeight int64) (*ctypes.ResultBlockchainInfo, error) { | |||
r, err := w.Client.BlockchainInfo(minHeight, maxHeight) | |||
if err != nil { | |||
return nil, err | |||
} | |||
// go and verify every blockmeta in the result.... | |||
for _, meta := range r.BlockMetas { | |||
// get a checkpoint to verify from | |||
c, err := w.Commit(&meta.Header.Height) | |||
if err != nil { | |||
return nil, err | |||
} | |||
check := certclient.CommitFromResult(c) | |||
err = ValidateBlockMeta(meta, check) | |||
if err != nil { | |||
return nil, err | |||
} | |||
} | |||
return r, nil | |||
} | |||
// Block returns an entire block and verifies all signatures | |||
func (w Wrapper) Block(height *int64) (*ctypes.ResultBlock, error) { | |||
r, err := w.Client.Block(height) | |||
if err != nil { | |||
return nil, err | |||
} | |||
// get a checkpoint to verify from | |||
c, err := w.Commit(height) | |||
if err != nil { | |||
return nil, err | |||
} | |||
check := certclient.CommitFromResult(c) | |||
// now verify | |||
err = ValidateBlockMeta(r.BlockMeta, check) | |||
if err != nil { | |||
return nil, err | |||
} | |||
err = ValidateBlock(r.Block, check) | |||
if err != nil { | |||
return nil, err | |||
} | |||
return r, nil | |||
} | |||
// Commit downloads the Commit and certifies it with the lite. | |||
// | |||
// This is the foundation for all other verification in this module | |||
func (w Wrapper) Commit(height *int64) (*ctypes.ResultCommit, error) { | |||
rpcclient.WaitForHeight(w.Client, *height, nil) | |||
r, err := w.Client.Commit(height) | |||
// if we got it, then certify it | |||
if err == nil { | |||
check := certclient.CommitFromResult(r) | |||
err = w.cert.Certify(check) | |||
} | |||
return r, err | |||
} | |||
// // WrappedSwitch creates a websocket connection that auto-verifies any info | |||
// // coming through before passing it along. | |||
// // | |||
// // Since the verification takes 1-2 rpc calls, this is obviously only for | |||
// // relatively low-throughput situations that can tolerate a bit extra latency | |||
// type WrappedSwitch struct { | |||
// types.EventSwitch | |||
// client rpcclient.Client | |||
// } | |||
// // FireEvent verifies any block or header returned from the eventswitch | |||
// func (s WrappedSwitch) FireEvent(event string, data events.EventData) { | |||
// tm, ok := data.(types.TMEventData) | |||
// if !ok { | |||
// fmt.Printf("bad type %#v\n", data) | |||
// return | |||
// } | |||
// // check to validate it if possible, and drop if not valid | |||
// switch t := tm.Unwrap().(type) { | |||
// case types.EventDataNewBlockHeader: | |||
// err := verifyHeader(s.client, t.Header) | |||
// if err != nil { | |||
// fmt.Printf("Invalid header: %#v\n", err) | |||
// return | |||
// } | |||
// case types.EventDataNewBlock: | |||
// err := verifyBlock(s.client, t.Block) | |||
// if err != nil { | |||
// fmt.Printf("Invalid block: %#v\n", err) | |||
// return | |||
// } | |||
// // TODO: can we verify tx as well? anything else | |||
// } | |||
// // looks good, we fire it | |||
// s.EventSwitch.FireEvent(event, data) | |||
// } | |||
// func verifyHeader(c rpcclient.Client, head *types.Header) error { | |||
// // get a checkpoint to verify from | |||
// commit, err := c.Commit(&head.Height) | |||
// if err != nil { | |||
// return err | |||
// } | |||
// check := certclient.CommitFromResult(commit) | |||
// return ValidateHeader(head, check) | |||
// } | |||
// | |||
// func verifyBlock(c rpcclient.Client, block *types.Block) error { | |||
// // get a checkpoint to verify from | |||
// commit, err := c.Commit(&block.Height) | |||
// if err != nil { | |||
// return err | |||
// } | |||
// check := certclient.CommitFromResult(commit) | |||
// return ValidateBlock(block, check) | |||
// } |
@ -1,65 +0,0 @@ | |||
/* | |||
cutWALUntil is a small utility for cutting a WAL until the given height | |||
(inclusively). Note it does not include last cs.EndHeightMessage. | |||
Usage: | |||
cutWALUntil <path-to-wal> height-to-stop <output-wal> | |||
*/ | |||
package main | |||
import ( | |||
"fmt" | |||
"io" | |||
"os" | |||
"strconv" | |||
cs "github.com/tendermint/tendermint/consensus" | |||
) | |||
func main() { | |||
if len(os.Args) < 4 { | |||
fmt.Println("3 arguments required: <path-to-wal> <height-to-stop> <output-wal>") | |||
os.Exit(1) | |||
} | |||
var heightToStop int64 | |||
var err error | |||
if heightToStop, err = strconv.ParseInt(os.Args[2], 10, 64); err != nil { | |||
panic(fmt.Errorf("failed to parse height: %v", err)) | |||
} | |||
in, err := os.Open(os.Args[1]) | |||
if err != nil { | |||
panic(fmt.Errorf("failed to open input WAL file: %v", err)) | |||
} | |||
defer in.Close() | |||
out, err := os.Create(os.Args[3]) | |||
if err != nil { | |||
panic(fmt.Errorf("failed to open output WAL file: %v", err)) | |||
} | |||
defer out.Close() | |||
enc := cs.NewWALEncoder(out) | |||
dec := cs.NewWALDecoder(in) | |||
for { | |||
msg, err := dec.Decode() | |||
if err == io.EOF { | |||
break | |||
} else if err != nil { | |||
panic(fmt.Errorf("failed to decode msg: %v", err)) | |||
} | |||
if m, ok := msg.Msg.(cs.EndHeightMessage); ok { | |||
if m.Height == heightToStop { | |||
break | |||
} | |||
} | |||
err = enc.Encode(msg) | |||
if err != nil { | |||
panic(fmt.Errorf("failed to encode msg: %v", err)) | |||
} | |||
} | |||
} |