@ -0,0 +1,114 @@ | |||
# Light client | |||
A light client is a process that connects to the Tendermint Full Node(s) and then tries to verify the Merkle proofs | |||
about the blockchain application. In this document we describe mechanisms that ensures that the Tendermint light client | |||
has the same level of security as Full Node processes (without being itself a Full Node). | |||
To be able to validate a Merkle proof, a light client needs to validate the blockchain header that contains the root app hash. | |||
Validating a blockchain header in Tendermint consists in verifying that the header is committed (signed) by >2/3 of the | |||
voting power of the corresponding validator set. As the validator set is a dynamic set (it is changing), one of the | |||
core functionality of the light client is updating the current validator set, that is then used to verify the | |||
blockchain header, and further the corresponding Merkle proofs. | |||
For the purpose of this light client specification, we assume that the Tendermint Full Node exposes the following functions over | |||
Tendermint RPC: | |||
```golang | |||
Header(height int64) (SignedHeader, error) // returns signed header for the given height | |||
Validators(height int64) (ResultValidators, error) // returns validator set for the given height | |||
LastHeader(valSetNumber int64) (SignedHeader, error) // returns last header signed by the validator set with the given validator set number | |||
type SignedHeader struct { | |||
Header Header | |||
Commit Commit | |||
ValSetNumber int64 | |||
} | |||
type ResultValidators struct { | |||
BlockHeight int64 | |||
Validators []Validator | |||
// time the current validator set is initialised, i.e, time of the last validator change before header BlockHeight | |||
ValSetTime int64 | |||
} | |||
``` | |||
We assume that Tendermint keeps track of the validator set changes and that each time a validator set is changed it is | |||
being assigned the next sequence number. We can call this number the validator set sequence number. Tendermint also remembers | |||
the Time from the header when the next validator set is initialised (starts to be in power), and we refer to this time | |||
as validator set init time. | |||
Furthermore, we assume that each validator set change is signed (committed) by the current validator set. More precisely, | |||
given a block `H` that contains transactions that are modifying the current validator set, the Merkle root hash of the next | |||
validator set (modified based on transactions from block H) will be in block `H+1` (and signed by the current validator | |||
set), and then starting from the block `H+2`, it will be signed by the next validator set. | |||
Note that the real Tendermint RPC API is slightly different (for example, response messages contain more data and function | |||
names are slightly different); we shortened (and modified) it for the purpose of this document to make the spec more | |||
clear and simple. Furthermore, note that in case of the third function, the returned header has `ValSetNumber` equals to | |||
`valSetNumber+1`. | |||
Locally, light client manages the following state: | |||
```golang | |||
valSet []Validator // current validator set (last known and verified validator set) | |||
valSetNumber int64 // sequence number of the current validator set | |||
valSetHash []byte // hash of the current validator set | |||
valSetTime int64 // time when the current validator set is initialised | |||
``` | |||
The light client is initialised with the trusted validator set, for example based on the known validator set hash, | |||
validator set sequence number and the validator set init time. | |||
The core of the light client logic is captured by the VerifyAndUpdate function that is used to 1) verify if the given header is valid, | |||
and 2) update the validator set (when the given header is valid and it is more recent than the seen headers). | |||
```golang | |||
VerifyAndUpdate(signedHeader SignedHeader): | |||
assertThat signedHeader.valSetNumber >= valSetNumber | |||
if isValid(signedHeader) and signedHeader.Header.Time <= valSetTime + UNBONDING_PERIOD then | |||
setValidatorSet(signedHeader) | |||
return true | |||
else | |||
updateValidatorSet(signedHeader.ValSetNumber) | |||
return VerifyAndUpdate(signedHeader) | |||
isValid(signedHeader SignedHeader): | |||
valSetOfTheHeader = Validators(signedHeader.Header.Height) | |||
assertThat Hash(valSetOfTheHeader) == signedHeader.Header.ValSetHash | |||
assertThat signedHeader is passing basic validation | |||
if votingPower(signedHeader.Commit) > 2/3 * votingPower(valSetOfTheHeader) then return true | |||
else | |||
return false | |||
setValidatorSet(signedHeader SignedHeader): | |||
nextValSet = Validators(signedHeader.Header.Height) | |||
assertThat Hash(nextValSet) == signedHeader.Header.ValidatorsHash | |||
valSet = nextValSet.Validators | |||
valSetHash = signedHeader.Header.ValidatorsHash | |||
valSetNumber = signedHeader.ValSetNumber | |||
valSetTime = nextValSet.ValSetTime | |||
votingPower(commit Commit): | |||
votingPower = 0 | |||
for each precommit in commit.Precommits do: | |||
if precommit.ValidatorAddress is in valSet and signature of the precommit verifies then | |||
votingPower += valSet[precommit.ValidatorAddress].VotingPower | |||
return votingPower | |||
votingPower(validatorSet []Validator): | |||
for each validator in validatorSet do: | |||
votingPower += validator.VotingPower | |||
return votingPower | |||
updateValidatorSet(valSetNumberOfTheHeader): | |||
while valSetNumber != valSetNumberOfTheHeader do | |||
signedHeader = LastHeader(valSetNumber) | |||
if isValid(signedHeader) then | |||
setValidatorSet(signedHeader) | |||
else return error | |||
return | |||
``` | |||
Note that in the logic above we assume that the light client will always go upward with respect to header verifications, | |||
i.e., that it will always be used to verify more recent headers. In case a light client needs to be used to verify older | |||
headers (go backward) the same mechanisms and similar logic can be used. In case a call to the FullNode or subsequent | |||
checks fail, a light client need to implement some recovery strategy, for example connecting to other FullNode. |
@ -0,0 +1,218 @@ | |||
package proxy_test | |||
import ( | |||
"testing" | |||
"time" | |||
"github.com/stretchr/testify/assert" | |||
"github.com/tendermint/tendermint/lite" | |||
"github.com/tendermint/tendermint/lite/proxy" | |||
"github.com/tendermint/tendermint/types" | |||
) | |||
var ( | |||
deadBeefTxs = types.Txs{[]byte("DE"), []byte("AD"), []byte("BE"), []byte("EF")} | |||
deadBeefHash = deadBeefTxs.Hash() | |||
testTime1 = time.Date(2018, 1, 1, 1, 1, 1, 1, time.UTC) | |||
testTime2 = time.Date(2017, 1, 2, 1, 1, 1, 1, time.UTC) | |||
) | |||
var hdrHeight11 = &types.Header{ | |||
Height: 11, | |||
Time: testTime1, | |||
ValidatorsHash: []byte("Tendermint"), | |||
} | |||
func TestValidateBlock(t *testing.T) { | |||
tests := []struct { | |||
block *types.Block | |||
commit lite.Commit | |||
wantErr string | |||
}{ | |||
{ | |||
block: nil, wantErr: "non-nil Block", | |||
}, | |||
{ | |||
block: &types.Block{}, wantErr: "nil Header", | |||
}, | |||
{ | |||
block: &types.Block{Header: new(types.Header)}, | |||
}, | |||
// Start Header.Height mismatch test | |||
{ | |||
block: &types.Block{Header: &types.Header{Height: 10}}, | |||
commit: lite.Commit{Header: &types.Header{Height: 11}}, | |||
wantErr: "don't match - 10 vs 11", | |||
}, | |||
{ | |||
block: &types.Block{Header: &types.Header{Height: 11}}, | |||
commit: lite.Commit{Header: &types.Header{Height: 11}}, | |||
}, | |||
// End Header.Height mismatch test | |||
// Start Header.Hash mismatch test | |||
{ | |||
block: &types.Block{Header: hdrHeight11}, | |||
commit: lite.Commit{Header: &types.Header{Height: 11}}, | |||
wantErr: "Headers don't match", | |||
}, | |||
{ | |||
block: &types.Block{Header: hdrHeight11}, | |||
commit: lite.Commit{Header: hdrHeight11}, | |||
}, | |||
// End Header.Hash mismatch test | |||
// Start Header.Data hash mismatch test | |||
{ | |||
block: &types.Block{ | |||
Header: &types.Header{Height: 11}, | |||
Data: &types.Data{Txs: []types.Tx{[]byte("0xDE"), []byte("AD")}}, | |||
}, | |||
commit: lite.Commit{ | |||
Header: &types.Header{Height: 11}, | |||
Commit: &types.Commit{BlockID: types.BlockID{Hash: []byte("0xDEADBEEF")}}, | |||
}, | |||
wantErr: "Data hash doesn't match header", | |||
}, | |||
{ | |||
block: &types.Block{ | |||
Header: &types.Header{Height: 11, DataHash: deadBeefHash}, | |||
Data: &types.Data{Txs: deadBeefTxs}, | |||
}, | |||
commit: lite.Commit{ | |||
Header: &types.Header{Height: 11}, | |||
Commit: &types.Commit{BlockID: types.BlockID{Hash: []byte("DEADBEEF")}}, | |||
}, | |||
}, | |||
// End Header.Data hash mismatch test | |||
} | |||
for i, tt := range tests { | |||
err := proxy.ValidateBlock(tt.block, tt.commit) | |||
if tt.wantErr != "" { | |||
if err == nil { | |||
assert.FailNowf(t, "Unexpectedly passed", "#%d", i) | |||
} else { | |||
assert.Contains(t, err.Error(), tt.wantErr, "#%d should contain the substring\n\n", i) | |||
} | |||
continue | |||
} | |||
assert.Nil(t, err, "#%d: expecting a nil error", i) | |||
} | |||
} | |||
func TestValidateBlockMeta(t *testing.T) { | |||
tests := []struct { | |||
meta *types.BlockMeta | |||
commit lite.Commit | |||
wantErr string | |||
}{ | |||
{ | |||
meta: nil, wantErr: "non-nil BlockMeta", | |||
}, | |||
{ | |||
meta: &types.BlockMeta{}, wantErr: "non-nil Header", | |||
}, | |||
{ | |||
meta: &types.BlockMeta{Header: new(types.Header)}, | |||
}, | |||
// Start Header.Height mismatch test | |||
{ | |||
meta: &types.BlockMeta{Header: &types.Header{Height: 10}}, | |||
commit: lite.Commit{Header: &types.Header{Height: 11}}, | |||
wantErr: "don't match - 10 vs 11", | |||
}, | |||
{ | |||
meta: &types.BlockMeta{Header: &types.Header{Height: 11}}, | |||
commit: lite.Commit{Header: &types.Header{Height: 11}}, | |||
}, | |||
// End Header.Height mismatch test | |||
// Start Headers don't match test | |||
{ | |||
meta: &types.BlockMeta{Header: hdrHeight11}, | |||
commit: lite.Commit{Header: &types.Header{Height: 11}}, | |||
wantErr: "Headers don't match", | |||
}, | |||
{ | |||
meta: &types.BlockMeta{Header: hdrHeight11}, | |||
commit: lite.Commit{Header: hdrHeight11}, | |||
}, | |||
{ | |||
meta: &types.BlockMeta{ | |||
Header: &types.Header{ | |||
Height: 11, | |||
ValidatorsHash: []byte("lite-test"), | |||
// TODO: should be able to use empty time after Amino upgrade | |||
Time: testTime1, | |||
}, | |||
}, | |||
commit: lite.Commit{ | |||
Header: &types.Header{Height: 11, DataHash: deadBeefHash}, | |||
}, | |||
wantErr: "Headers don't match", | |||
}, | |||
{ | |||
meta: &types.BlockMeta{ | |||
Header: &types.Header{ | |||
Height: 11, DataHash: deadBeefHash, | |||
ValidatorsHash: []byte("Tendermint"), | |||
Time: testTime1, | |||
}, | |||
}, | |||
commit: lite.Commit{ | |||
Header: &types.Header{ | |||
Height: 11, DataHash: deadBeefHash, | |||
ValidatorsHash: []byte("Tendermint"), | |||
Time: testTime2, | |||
}, | |||
Commit: &types.Commit{BlockID: types.BlockID{Hash: []byte("DEADBEEF")}}, | |||
}, | |||
wantErr: "Headers don't match", | |||
}, | |||
{ | |||
meta: &types.BlockMeta{ | |||
Header: &types.Header{ | |||
Height: 11, DataHash: deadBeefHash, | |||
ValidatorsHash: []byte("Tendermint"), | |||
Time: testTime2, | |||
}, | |||
}, | |||
commit: lite.Commit{ | |||
Header: &types.Header{ | |||
Height: 11, DataHash: deadBeefHash, | |||
ValidatorsHash: []byte("Tendermint-x"), | |||
Time: testTime2, | |||
}, | |||
Commit: &types.Commit{BlockID: types.BlockID{Hash: []byte("DEADBEEF")}}, | |||
}, | |||
wantErr: "Headers don't match", | |||
}, | |||
// End Headers don't match test | |||
} | |||
for i, tt := range tests { | |||
err := proxy.ValidateBlockMeta(tt.meta, tt.commit) | |||
if tt.wantErr != "" { | |||
if err == nil { | |||
assert.FailNowf(t, "Unexpectedly passed", "#%d: wanted error %q", i, tt.wantErr) | |||
} else { | |||
assert.Contains(t, err.Error(), tt.wantErr, "#%d should contain the substring\n\n", i) | |||
} | |||
continue | |||
} | |||
assert.Nil(t, err, "#%d: expecting a nil error", i) | |||
} | |||
} |
@ -0,0 +1,72 @@ | |||
package dummy | |||
import ( | |||
p2p "github.com/tendermint/tendermint/p2p" | |||
tmconn "github.com/tendermint/tendermint/p2p/conn" | |||
cmn "github.com/tendermint/tmlibs/common" | |||
) | |||
type peer struct { | |||
cmn.BaseService | |||
kv map[string]interface{} | |||
} | |||
var _ p2p.Peer = (*peer)(nil) | |||
// NewPeer creates new dummy peer. | |||
func NewPeer() *peer { | |||
p := &peer{ | |||
kv: make(map[string]interface{}), | |||
} | |||
p.BaseService = *cmn.NewBaseService(nil, "peer", p) | |||
return p | |||
} | |||
// ID always returns dummy. | |||
func (p *peer) ID() p2p.ID { | |||
return p2p.ID("dummy") | |||
} | |||
// IsOutbound always returns false. | |||
func (p *peer) IsOutbound() bool { | |||
return false | |||
} | |||
// IsPersistent always returns false. | |||
func (p *peer) IsPersistent() bool { | |||
return false | |||
} | |||
// NodeInfo always returns empty node info. | |||
func (p *peer) NodeInfo() p2p.NodeInfo { | |||
return p2p.NodeInfo{} | |||
} | |||
// Status always returns empry connection status. | |||
func (p *peer) Status() tmconn.ConnectionStatus { | |||
return tmconn.ConnectionStatus{} | |||
} | |||
// Send does not do anything and just returns true. | |||
func (p *peer) Send(byte, interface{}) bool { | |||
return true | |||
} | |||
// TrySend does not do anything and just returns true. | |||
func (p *peer) TrySend(byte, interface{}) bool { | |||
return true | |||
} | |||
// Set records value under key specified in the map. | |||
func (p *peer) Set(key string, value interface{}) { | |||
p.kv[key] = value | |||
} | |||
// Get returns a value associated with the key. Nil is returned if no value | |||
// found. | |||
func (p *peer) Get(key string) interface{} { | |||
if value, ok := p.kv[key]; ok { | |||
return value | |||
} | |||
return nil | |||
} |
@ -0,0 +1,31 @@ | |||
package core | |||
import ( | |||
ctypes "github.com/tendermint/tendermint/rpc/core/types" | |||
) | |||
// Get node health. Returns empty result (200 OK) on success, no response - in | |||
// case of an error. | |||
// | |||
// ```shell | |||
// curl 'localhost:46657/health' | |||
// ``` | |||
// | |||
// ```go | |||
// client := client.NewHTTP("tcp://0.0.0.0:46657", "/websocket") | |||
// result, err := client.Health() | |||
// ``` | |||
// | |||
// > The above command returns JSON structured like this: | |||
// | |||
// ```json | |||
// { | |||
// "error": "", | |||
// "result": {}, | |||
// "id": "", | |||
// "jsonrpc": "2.0" | |||
// } | |||
// ``` | |||
func Health() (*ctypes.ResultHealth, error) { | |||
return &ctypes.ResultHealth{}, nil | |||
} |
@ -1,35 +0,0 @@ | |||
#! /bin/bash | |||
set -ex | |||
set +u | |||
if [[ "$DEP" == "" ]]; then | |||
DEP=$GOPATH/src/github.com/tendermint/tendermint/Gopkg.lock | |||
fi | |||
set -u | |||
set -u | |||
function getVendoredVersion() { | |||
grep -A100 "$LIB" "$DEP" | grep revision | head -n1 | grep -o '"[^"]\+"' | cut -d '"' -f 2 | |||
} | |||
# fetch and checkout vendored dep | |||
lib=$1 | |||
echo "----------------------------------" | |||
echo "Getting $lib ..." | |||
go get -t "github.com/tendermint/$lib/..." | |||
VENDORED=$(getVendoredVersion "$lib") | |||
cd "$GOPATH/src/github.com/tendermint/$lib" || exit | |||
MASTER=$(git rev-parse origin/master) | |||
if [[ "$VENDORED" != "$MASTER" ]]; then | |||
echo "... VENDORED != MASTER ($VENDORED != $MASTER)" | |||
echo "... Checking out commit $VENDORED" | |||
git checkout "$VENDORED" &> /dev/null | |||
fi |
@ -1,34 +0,0 @@ | |||
#! /bin/bash | |||
set -ex | |||
export PATH="$GOBIN:$PATH" | |||
# 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 )" | |||
#################### | |||
# libs we depend on | |||
#################### | |||
# All libs should define `make test` and `make get_vendor_deps` | |||
LIBS=(tmlibs go-wire go-crypto abci) | |||
for lib in "${LIBS[@]}"; do | |||
# checkout vendored version of lib | |||
bash scripts/dep_utils/checkout.sh "$lib" | |||
echo "Testing $lib ..." | |||
cd "$GOPATH/src/github.com/tendermint/$lib" | |||
make get_tools | |||
make get_vendor_deps | |||
make test | |||
if [[ "$?" != 0 ]]; then | |||
echo "FAIL" | |||
exit 1 | |||
fi | |||
cd "$DIR" | |||
done | |||
echo "" | |||
echo "PASS" |
@ -0,0 +1,238 @@ | |||
package types | |||
import ( | |||
"bytes" | |||
"encoding/json" | |||
"errors" | |||
"fmt" | |||
"time" | |||
crypto "github.com/tendermint/go-crypto" | |||
"github.com/tendermint/tendermint/types" | |||
cmn "github.com/tendermint/tmlibs/common" | |||
) | |||
// TODO: type ? | |||
const ( | |||
stepNone int8 = 0 // Used to distinguish the initial state | |||
stepPropose int8 = 1 | |||
stepPrevote int8 = 2 | |||
stepPrecommit int8 = 3 | |||
) | |||
func voteToStep(vote *types.Vote) int8 { | |||
switch vote.Type { | |||
case types.VoteTypePrevote: | |||
return stepPrevote | |||
case types.VoteTypePrecommit: | |||
return stepPrecommit | |||
default: | |||
panic("Unknown vote type") | |||
} | |||
} | |||
//------------------------------------- | |||
// LastSignedInfo contains information about the latest | |||
// data signed by a validator to help prevent double signing. | |||
type LastSignedInfo struct { | |||
Height int64 `json:"height"` | |||
Round int `json:"round"` | |||
Step int8 `json:"step"` | |||
Signature crypto.Signature `json:"signature,omitempty"` // so we dont lose signatures | |||
SignBytes cmn.HexBytes `json:"signbytes,omitempty"` // so we dont lose signatures | |||
} | |||
func NewLastSignedInfo() *LastSignedInfo { | |||
return &LastSignedInfo{ | |||
Step: stepNone, | |||
} | |||
} | |||
func (lsi *LastSignedInfo) String() string { | |||
return fmt.Sprintf("LH:%v, LR:%v, LS:%v", lsi.Height, lsi.Round, lsi.Step) | |||
} | |||
// Verify returns an error if there is a height/round/step regression | |||
// or if the HRS matches but there are no LastSignBytes. | |||
// It returns true if HRS matches exactly and the LastSignature exists. | |||
// It panics if the HRS matches, the LastSignBytes are not empty, but the LastSignature is empty. | |||
func (lsi LastSignedInfo) Verify(height int64, round int, step int8) (bool, error) { | |||
if lsi.Height > height { | |||
return false, errors.New("Height regression") | |||
} | |||
if lsi.Height == height { | |||
if lsi.Round > round { | |||
return false, errors.New("Round regression") | |||
} | |||
if lsi.Round == round { | |||
if lsi.Step > step { | |||
return false, errors.New("Step regression") | |||
} else if lsi.Step == step { | |||
if lsi.SignBytes != nil { | |||
if lsi.Signature.Empty() { | |||
panic("info: LastSignature is nil but LastSignBytes is not!") | |||
} | |||
return true, nil | |||
} | |||
return false, errors.New("No LastSignature found") | |||
} | |||
} | |||
} | |||
return false, nil | |||
} | |||
// Set height/round/step and signature on the info | |||
func (lsi *LastSignedInfo) Set(height int64, round int, step int8, | |||
signBytes []byte, sig crypto.Signature) { | |||
lsi.Height = height | |||
lsi.Round = round | |||
lsi.Step = step | |||
lsi.Signature = sig | |||
lsi.SignBytes = signBytes | |||
} | |||
// Reset resets all the values. | |||
// XXX: Unsafe. | |||
func (lsi *LastSignedInfo) Reset() { | |||
lsi.Height = 0 | |||
lsi.Round = 0 | |||
lsi.Step = 0 | |||
lsi.Signature = crypto.Signature{} | |||
lsi.SignBytes = nil | |||
} | |||
// SignVote checks the height/round/step (HRS) are greater than the latest state of the LastSignedInfo. | |||
// If so, it signs the vote, updates the LastSignedInfo, and sets the signature on the vote. | |||
// If the HRS are equal and the only thing changed is the timestamp, it sets the vote.Timestamp to the previous | |||
// value and the Signature to the LastSignedInfo.Signature. | |||
// Else it returns an error. | |||
func (lsi *LastSignedInfo) SignVote(signer types.Signer, chainID string, vote *types.Vote) error { | |||
height, round, step := vote.Height, vote.Round, voteToStep(vote) | |||
signBytes := vote.SignBytes(chainID) | |||
sameHRS, err := lsi.Verify(height, round, step) | |||
if err != nil { | |||
return err | |||
} | |||
// We might crash before writing to the wal, | |||
// causing us to try to re-sign for the same HRS. | |||
// If signbytes are the same, use the last signature. | |||
// If they only differ by timestamp, use last timestamp and signature | |||
// Otherwise, return error | |||
if sameHRS { | |||
if bytes.Equal(signBytes, lsi.SignBytes) { | |||
vote.Signature = lsi.Signature | |||
} else if timestamp, ok := checkVotesOnlyDifferByTimestamp(lsi.SignBytes, signBytes); ok { | |||
vote.Timestamp = timestamp | |||
vote.Signature = lsi.Signature | |||
} else { | |||
err = fmt.Errorf("Conflicting data") | |||
} | |||
return err | |||
} | |||
sig, err := signer.Sign(signBytes) | |||
if err != nil { | |||
return err | |||
} | |||
lsi.Set(height, round, step, signBytes, sig) | |||
vote.Signature = sig | |||
return nil | |||
} | |||
// SignProposal checks if the height/round/step (HRS) are greater than the latest state of the LastSignedInfo. | |||
// If so, it signs the proposal, updates the LastSignedInfo, and sets the signature on the proposal. | |||
// If the HRS are equal and the only thing changed is the timestamp, it sets the timestamp to the previous | |||
// value and the Signature to the LastSignedInfo.Signature. | |||
// Else it returns an error. | |||
func (lsi *LastSignedInfo) SignProposal(signer types.Signer, chainID string, proposal *types.Proposal) error { | |||
height, round, step := proposal.Height, proposal.Round, stepPropose | |||
signBytes := proposal.SignBytes(chainID) | |||
sameHRS, err := lsi.Verify(height, round, step) | |||
if err != nil { | |||
return err | |||
} | |||
// We might crash before writing to the wal, | |||
// causing us to try to re-sign for the same HRS. | |||
// If signbytes are the same, use the last signature. | |||
// If they only differ by timestamp, use last timestamp and signature | |||
// Otherwise, return error | |||
if sameHRS { | |||
if bytes.Equal(signBytes, lsi.SignBytes) { | |||
proposal.Signature = lsi.Signature | |||
} else if timestamp, ok := checkProposalsOnlyDifferByTimestamp(lsi.SignBytes, signBytes); ok { | |||
proposal.Timestamp = timestamp | |||
proposal.Signature = lsi.Signature | |||
} else { | |||
err = fmt.Errorf("Conflicting data") | |||
} | |||
return err | |||
} | |||
sig, err := signer.Sign(signBytes) | |||
if err != nil { | |||
return err | |||
} | |||
lsi.Set(height, round, step, signBytes, sig) | |||
proposal.Signature = sig | |||
return nil | |||
} | |||
//------------------------------------- | |||
// returns the timestamp from the lastSignBytes. | |||
// returns true if the only difference in the votes is their timestamp. | |||
func checkVotesOnlyDifferByTimestamp(lastSignBytes, newSignBytes []byte) (time.Time, bool) { | |||
var lastVote, newVote types.CanonicalJSONOnceVote | |||
if err := json.Unmarshal(lastSignBytes, &lastVote); err != nil { | |||
panic(fmt.Sprintf("LastSignBytes cannot be unmarshalled into vote: %v", err)) | |||
} | |||
if err := json.Unmarshal(newSignBytes, &newVote); err != nil { | |||
panic(fmt.Sprintf("signBytes cannot be unmarshalled into vote: %v", err)) | |||
} | |||
lastTime, err := time.Parse(types.TimeFormat, lastVote.Vote.Timestamp) | |||
if err != nil { | |||
panic(err) | |||
} | |||
// set the times to the same value and check equality | |||
now := types.CanonicalTime(time.Now()) | |||
lastVote.Vote.Timestamp = now | |||
newVote.Vote.Timestamp = now | |||
lastVoteBytes, _ := json.Marshal(lastVote) | |||
newVoteBytes, _ := json.Marshal(newVote) | |||
return lastTime, bytes.Equal(newVoteBytes, lastVoteBytes) | |||
} | |||
// returns the timestamp from the lastSignBytes. | |||
// returns true if the only difference in the proposals is their timestamp | |||
func checkProposalsOnlyDifferByTimestamp(lastSignBytes, newSignBytes []byte) (time.Time, bool) { | |||
var lastProposal, newProposal types.CanonicalJSONOnceProposal | |||
if err := json.Unmarshal(lastSignBytes, &lastProposal); err != nil { | |||
panic(fmt.Sprintf("LastSignBytes cannot be unmarshalled into proposal: %v", err)) | |||
} | |||
if err := json.Unmarshal(newSignBytes, &newProposal); err != nil { | |||
panic(fmt.Sprintf("signBytes cannot be unmarshalled into proposal: %v", err)) | |||
} | |||
lastTime, err := time.Parse(types.TimeFormat, lastProposal.Proposal.Timestamp) | |||
if err != nil { | |||
panic(err) | |||
} | |||
// set the times to the same value and check equality | |||
now := types.CanonicalTime(time.Now()) | |||
lastProposal.Proposal.Timestamp = now | |||
newProposal.Proposal.Timestamp = now | |||
lastProposalBytes, _ := json.Marshal(lastProposal) | |||
newProposalBytes, _ := json.Marshal(newProposal) | |||
return lastTime, bytes.Equal(newProposalBytes, lastProposalBytes) | |||
} |