Skip to content

Commit

Permalink
state-proof-based catchup
Browse files Browse the repository at this point in the history
Add support for catchup to use state proofs to validate new blocks.

The main changes are:

- The catchup service now has a state proof fetcher, whose job is to
  retrieve state proofs for rounds beyond the current ledger state.
  These state proofs are stored in an sqlite DB, since there might be
  many state proofs that we need to fetch.

- The catchup service exposes a way to set trusted "renaissance"
  parameters for authenticating the initial state proofs, for cases
  when we can't use state proofs from the genesis block (like the
  situation we have on mainnet now).

- The BlockService HTTP interface adds support for retrieving a
  state proof, and for getting a light block header proof instead
  of a cert when retrieving a block.

- The catchup service uses state proofs, if possible, to authenticate
  new blocks, in lieu of agreement certificates.  The catchup service
  is backwards-compatible: if it requests a state proof from the
  BlockService, but receives an agreement certificate instead (e.g.,
  because the BlockService has not been upgraded with the above
  changes), the catchup service will validate the certificate instead.

- The config file has additional fields to optionally specify the
  renaissance catchup parameters, to allow catchup to start validating
  state proofs from some (trusted through out-of-band channels) block.

It may be a good idea, for performance, to pre-compute the state proofs
and distribute them in a single file, rather than asking many relays to
find the state proofs on-demand.  This is not done yet, because it largely
depends on how we would want to distribute these bundled state proofs.
It should be reasonably straightforward to feed a bundle of state proofs
into the stateProofFetcher.
  • Loading branch information
zeldovich committed Aug 31, 2023
1 parent 07bc04e commit 9d4edc4
Show file tree
Hide file tree
Showing 25 changed files with 1,668 additions and 189 deletions.
2 changes: 1 addition & 1 deletion catchup/catchpointService.go
Original file line number Diff line number Diff line change
Expand Up @@ -674,7 +674,7 @@ func (cs *CatchpointCatchupService) fetchBlock(round basics.Round, retryCount ui
return nil, time.Duration(0), psp, true, cs.abort(fmt.Errorf("fetchBlock: recurring non-HTTP peer was provided by the peer selector"))
}
fetcher := makeUniversalBlockFetcher(cs.log, cs.net, cs.config)
blk, _, downloadDuration, err = fetcher.fetchBlock(cs.ctx, round, httpPeer)
blk, _, _, downloadDuration, err = fetcher.fetchBlock(cs.ctx, round, httpPeer, false)
if err != nil {
if cs.ctx.Err() != nil {
return nil, time.Duration(0), psp, true, cs.stopOrAbort()
Expand Down
152 changes: 145 additions & 7 deletions catchup/fetcher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,25 +31,70 @@ import (
"github.com/algorand/go-algorand/components/mocks"
"github.com/algorand/go-algorand/config"
"github.com/algorand/go-algorand/crypto"
"github.com/algorand/go-algorand/crypto/merklearray"
"github.com/algorand/go-algorand/crypto/merklesignature"
cryptostateproof "github.com/algorand/go-algorand/crypto/stateproof"
"github.com/algorand/go-algorand/data"
"github.com/algorand/go-algorand/data/basics"
"github.com/algorand/go-algorand/data/bookkeeping"
"github.com/algorand/go-algorand/data/transactions"
"github.com/algorand/go-algorand/logging"
"github.com/algorand/go-algorand/network"
"github.com/algorand/go-algorand/protocol"
"github.com/algorand/go-algorand/stateproof"
)

func buildTestLedger(t *testing.T, blk bookkeeping.Block) (ledger *data.Ledger, next basics.Round, b bookkeeping.Block, err error) {
const (
testLedgerKeyValidRounds = 10000
)

type testLedgerStateProofData struct {
Params config.ConsensusParams
User basics.Address
Secrets *merklesignature.Secrets
TotalWeight basics.MicroAlgos
Participants basics.ParticipantsArray
Tree *merklearray.Tree
TemplateBlock bookkeeping.Block
}

func buildTestLedger(t *testing.T, blk bookkeeping.Block) (ledger *data.Ledger, next basics.Round, b bookkeeping.Block, stateProofData *testLedgerStateProofData, err error) {
var user basics.Address
user[0] = 123

proto := config.Consensus[protocol.ConsensusCurrentVersion]
genesis := make(map[basics.Address]basics.AccountData)
genesis[user] = basics.AccountData{
ver := blk.CurrentProtocol
if ver == "" {
ver = protocol.ConsensusCurrentVersion
}

proto := config.Consensus[ver]

userData := basics.AccountData{
Status: basics.Offline,
MicroAlgos: basics.MicroAlgos{Raw: proto.MinBalance * 2000000},
}

if proto.StateProofInterval > 0 {
stateProofData = &testLedgerStateProofData{
Params: proto,
User: user,
}

stateProofData.Secrets, err = merklesignature.New(0, testLedgerKeyValidRounds, proto.StateProofInterval)
if err != nil {
t.Fatal("couldn't generate state proof keys", err)
return
}

userData.StateProofID = stateProofData.Secrets.GetVerifier().Commitment
userData.VoteFirstValid = 0
userData.VoteLastValid = testLedgerKeyValidRounds
userData.VoteKeyDilution = 1
userData.Status = basics.Online
}

genesis := make(map[basics.Address]basics.AccountData)
genesis[user] = userData
genesis[sinkAddr] = basics.AccountData{
Status: basics.Offline,
MicroAlgos: basics.MicroAlgos{Raw: proto.MinBalance * 2000000},
Expand All @@ -66,7 +111,7 @@ func buildTestLedger(t *testing.T, blk bookkeeping.Block) (ledger *data.Ledger,
cfg := config.GetDefaultLocal()
cfg.Archival = true
ledger, err = data.LoadLedger(
log, t.Name(), inMem, protocol.ConsensusCurrentVersion, genBal, "", genHash,
log, t.Name(), inMem, ver, genBal, "", genHash,
nil, cfg,
)
if err != nil {
Expand Down Expand Up @@ -99,23 +144,105 @@ func buildTestLedger(t *testing.T, blk bookkeeping.Block) (ledger *data.Ledger,
b.RewardsLevel = prev.RewardsLevel
b.BlockHeader.Round = next
b.BlockHeader.GenesisHash = genHash
b.CurrentProtocol = protocol.ConsensusCurrentVersion
b.CurrentProtocol = ver
txib, err := b.EncodeSignedTxn(signedtx, transactions.ApplyData{})
require.NoError(t, err)
b.Payset = []transactions.SignedTxnInBlock{
txib,
}
b.TxnCommitments, err = b.PaysetCommit()
require.NoError(t, err)

if proto.StateProofInterval > 0 {
var p basics.Participant
p.Weight = userData.MicroAlgos.ToUint64()
p.PK.KeyLifetime = merklesignature.KeyLifetimeDefault
p.PK.Commitment = userData.StateProofID

stateProofData.Participants = append(stateProofData.Participants, p)
stateProofData.TotalWeight = userData.MicroAlgos
stateProofData.Tree, err = merklearray.BuildVectorCommitmentTree(stateProofData.Participants, crypto.HashFactory{HashType: cryptostateproof.HashType})
if err != nil {
t.Fatal("couldn't build state proof voters tree", err)
return
}

b.StateProofTracking = map[protocol.StateProofType]bookkeeping.StateProofTrackingData{
protocol.StateProofBasic: bookkeeping.StateProofTrackingData{
StateProofVotersCommitment: stateProofData.Tree.Root(),
StateProofOnlineTotalWeight: stateProofData.TotalWeight,
StateProofNextRound: basics.Round(proto.StateProofInterval),
},
}
}

require.NoError(t, ledger.AddBlock(b, agreement.Certificate{Round: next}))
return
}

func addBlocks(t *testing.T, ledger *data.Ledger, blk bookkeeping.Block, numBlocks int) {
func addBlocks(t *testing.T, ledger *data.Ledger, blk bookkeeping.Block, stateProofData *testLedgerStateProofData, numBlocks int) {
var err error
origPayset := blk.Payset
nextStateProofTracking := blk.StateProofTracking

for i := 0; i < numBlocks; i++ {
blk.BlockHeader.Round++
blk.BlockHeader.TimeStamp += int64(crypto.RandUint64() % 100 * 1000)
blk.Payset = origPayset
blk.StateProofTracking = nextStateProofTracking

if stateProofData != nil &&
(blk.BlockHeader.Round%basics.Round(stateProofData.Params.StateProofInterval)) == 0 &&
blk.BlockHeader.Round > basics.Round(stateProofData.Params.StateProofInterval) {
proofrnd := blk.BlockHeader.Round.SubSaturate(basics.Round(stateProofData.Params.StateProofInterval))
msg, err := stateproof.GenerateStateProofMessage(ledger, proofrnd)
require.NoError(t, err)

provenWeight, overflowed := basics.Muldiv(stateProofData.TotalWeight.ToUint64(), uint64(stateProofData.Params.StateProofWeightThreshold), 1<<32)
require.False(t, overflowed)

msgHash := msg.Hash()
prover, err := cryptostateproof.MakeProver(msgHash,
uint64(proofrnd),
provenWeight,
stateProofData.Participants,
stateProofData.Tree,
stateProofData.Params.StateProofStrengthTarget)
require.NoError(t, err)

sig, err := stateProofData.Secrets.GetSigner(uint64(proofrnd)).SignBytes(msgHash[:])
require.NoError(t, err)

err = prover.Add(0, sig)
require.NoError(t, err)

require.True(t, prover.Ready())
sp, err := prover.CreateProof()
require.NoError(t, err)

var stxn transactions.SignedTxn
stxn.Txn.Type = protocol.StateProofTx
stxn.Txn.Sender = transactions.StateProofSender
stxn.Txn.FirstValid = blk.BlockHeader.Round
stxn.Txn.LastValid = blk.BlockHeader.Round
stxn.Txn.GenesisHash = blk.BlockHeader.GenesisHash
stxn.Txn.StateProofTxnFields.StateProofType = protocol.StateProofBasic
stxn.Txn.StateProofTxnFields.StateProof = *sp
stxn.Txn.StateProofTxnFields.Message = msg

txib, err := blk.EncodeSignedTxn(stxn, transactions.ApplyData{})
require.NoError(t, err)
blk.Payset = make([]transactions.SignedTxnInBlock, len(origPayset)+1)
copy(blk.Payset[:], origPayset[:])
blk.Payset[len(origPayset)] = txib

sptracking := blk.StateProofTracking[protocol.StateProofBasic]
sptracking.StateProofNextRound = blk.BlockHeader.Round
nextStateProofTracking = map[protocol.StateProofType]bookkeeping.StateProofTrackingData{
protocol.StateProofBasic: sptracking,
}
}

blk.TxnCommitments, err = blk.PaysetCommit()
require.NoError(t, err)

Expand All @@ -126,6 +253,10 @@ func addBlocks(t *testing.T, ledger *data.Ledger, blk bookkeeping.Block, numBloc
require.NoError(t, err)
require.Equal(t, blk.BlockHeader, hdr)
}

blk.Payset = origPayset
blk.StateProofTracking = nextStateProofTracking
stateProofData.TemplateBlock = blk
}

type basicRPCNode struct {
Expand All @@ -143,6 +274,13 @@ func (b *basicRPCNode) RegisterHTTPHandler(path string, handler http.Handler) {
b.rmux.Handle(path, handler)
}

func (b *basicRPCNode) RegisterHTTPHandlerFunc(path string, handler func(response http.ResponseWriter, request *http.Request)) {
if b.rmux == nil {
b.rmux = mux.NewRouter()
}
b.rmux.HandleFunc(path, handler)
}

func (b *basicRPCNode) RegisterHandlers(dispatch []network.TaggedMessageHandler) {
}

Expand Down
2 changes: 1 addition & 1 deletion catchup/pref_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ func BenchmarkServiceFetchBlocks(b *testing.B) {
require.NoError(b, err)

// Make Service
syncer := MakeService(logging.TestingLog(b), defaultConfig, net, local, new(mockedAuthenticator), nil, nil)
syncer := MakeService(logging.TestingLog(b), defaultConfig, net, local, new(mockedAuthenticator), nil, nil, nil)
b.StartTimer()
syncer.Start()
for w := 0; w < 1000; w++ {
Expand Down
Loading

0 comments on commit 9d4edc4

Please sign in to comment.