mirror of https://github.com/poanetwork/quorum.git
Merge remote-tracking branch 'remotes/origin/master' into permissions-master-merge
# Conflicts: # params/config.go
This commit is contained in:
commit
1db7d6a36b
|
@ -81,7 +81,9 @@ type ContractTransactor interface {
|
|||
// for setting a reasonable default.
|
||||
EstimateGas(ctx context.Context, call ethereum.CallMsg) (gas uint64, err error)
|
||||
// SendTransaction injects the transaction into the pending pool for execution.
|
||||
SendTransaction(ctx context.Context, tx *types.Transaction) error
|
||||
SendTransaction(ctx context.Context, tx *types.Transaction, args PrivateTxArgs) error
|
||||
// PreparePrivateTransaction send the private transaction to Tessera/Constellation's /storeraw API using HTTP
|
||||
PreparePrivateTransaction(data []byte, privateFrom string) ([]byte, error)
|
||||
}
|
||||
|
||||
// ContractFilterer defines the methods needed to access log events using one-off
|
||||
|
|
|
@ -309,7 +309,7 @@ func (b *SimulatedBackend) callContract(ctx context.Context, call ethereum.CallM
|
|||
|
||||
// SendTransaction updates the pending block to include the given transaction.
|
||||
// It panics if the transaction is invalid.
|
||||
func (b *SimulatedBackend) SendTransaction(ctx context.Context, tx *types.Transaction) error {
|
||||
func (b *SimulatedBackend) SendTransaction(ctx context.Context, tx *types.Transaction, args bind.PrivateTxArgs) error {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
|
@ -335,6 +335,11 @@ func (b *SimulatedBackend) SendTransaction(ctx context.Context, tx *types.Transa
|
|||
return nil
|
||||
}
|
||||
|
||||
// PreparePrivateTransaction dummy implementation
|
||||
func (b *SimulatedBackend) PreparePrivateTransaction(data []byte, privateFrom string) ([]byte, error) {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// FilterLogs executes a log filter operation, blocking during execution and
|
||||
// returning all the results in one batch.
|
||||
//
|
||||
|
|
|
@ -34,6 +34,13 @@ import (
|
|||
// sign the transaction before submission.
|
||||
type SignerFn func(types.Signer, common.Address, *types.Transaction) (*types.Transaction, error)
|
||||
|
||||
// Quorum
|
||||
//
|
||||
// Additional arguments in order to support transaction privacy
|
||||
type PrivateTxArgs struct {
|
||||
PrivateFor []string `json:"privateFor"`
|
||||
}
|
||||
|
||||
// CallOpts is the collection of options to fine tune a contract call request.
|
||||
type CallOpts struct {
|
||||
Pending bool // Whether to operate on the pending state or the last known one
|
||||
|
@ -54,6 +61,10 @@ type TransactOpts struct {
|
|||
GasLimit uint64 // Gas limit to set for the transaction execution (0 = estimate)
|
||||
|
||||
Context context.Context // Network context to support cancellation and timeouts (nil = no timeout)
|
||||
|
||||
// Quorum
|
||||
PrivateFrom string // The public key of the Tessera/Constellation identity to send this tx from.
|
||||
PrivateFor []string // The public keys of the Tessera/Constellation identities this tx is intended for.
|
||||
}
|
||||
|
||||
// FilterOpts is the collection of options to fine tune filtering for events
|
||||
|
@ -231,16 +242,36 @@ func (c *BoundContract) transact(opts *TransactOpts, contract *common.Address, i
|
|||
} else {
|
||||
rawTx = types.NewTransaction(nonce, c.address, value, gasLimit, gasPrice, input)
|
||||
}
|
||||
|
||||
// If this transaction is private, we need to substitute the data payload
|
||||
// with the hash of the transaction from tessera/constellation.
|
||||
if opts.PrivateFor != nil {
|
||||
var payload []byte
|
||||
payload, err = c.transactor.PreparePrivateTransaction(rawTx.Data(), opts.PrivateFrom)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rawTx = c.createPrivateTransaction(rawTx, payload)
|
||||
}
|
||||
|
||||
// Choose signer to sign transaction
|
||||
if opts.Signer == nil {
|
||||
return nil, errors.New("no signer to authorize the transaction with")
|
||||
}
|
||||
signedTx, err := opts.Signer(types.HomesteadSigner{}, opts.From, rawTx)
|
||||
var signedTx *types.Transaction
|
||||
if rawTx.IsPrivate() {
|
||||
signedTx, err = opts.Signer(types.QuorumPrivateTxSigner{}, opts.From, rawTx)
|
||||
} else {
|
||||
signedTx, err = opts.Signer(types.HomesteadSigner{}, opts.From, rawTx)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := c.transactor.SendTransaction(ensureContext(opts.Context), signedTx); err != nil {
|
||||
|
||||
if err := c.transactor.SendTransaction(ensureContext(opts.Context), signedTx, PrivateTxArgs{PrivateFor: opts.PrivateFor}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return signedTx, nil
|
||||
}
|
||||
|
||||
|
@ -340,6 +371,18 @@ func (c *BoundContract) UnpackLog(out interface{}, event string, log types.Log)
|
|||
return parseTopics(out, indexed, log.Topics[1:])
|
||||
}
|
||||
|
||||
// createPrivateTransaction replaces the payload of private transaction to the hash from Tessera/Constellation
|
||||
func (c *BoundContract) createPrivateTransaction(tx *types.Transaction, payload []byte) *types.Transaction {
|
||||
var privateTx *types.Transaction
|
||||
if tx.To() == nil {
|
||||
privateTx = types.NewContractCreation(tx.Nonce(), tx.Value(), tx.Gas(), tx.GasPrice(), payload)
|
||||
} else {
|
||||
privateTx = types.NewTransaction(tx.Nonce(), c.address, tx.Value(), tx.Gas(), tx.GasPrice(), payload)
|
||||
}
|
||||
privateTx.SetPrivate()
|
||||
return privateTx
|
||||
}
|
||||
|
||||
// ensureContext is a helper method to ensure a context is not nil, even if the
|
||||
// user specified it as such.
|
||||
func ensureContext(ctx context.Context) context.Context {
|
||||
|
|
|
@ -76,7 +76,7 @@ func TestWaitDeployed(t *testing.T) {
|
|||
}()
|
||||
|
||||
// Send and mine the transaction.
|
||||
backend.SendTransaction(ctx, tx)
|
||||
backend.SendTransaction(ctx, tx, bind.PrivateTxArgs{})
|
||||
backend.Commit()
|
||||
|
||||
select {
|
||||
|
|
|
@ -41,6 +41,8 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts/abi/bind"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts"
|
||||
"github.com/ethereum/go-ethereum/accounts/keystore"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
|
@ -483,7 +485,7 @@ func (f *faucet) apiHandler(conn *websocket.Conn) {
|
|||
continue
|
||||
}
|
||||
// Submit the transaction and mark as funded if successful
|
||||
if err := f.client.SendTransaction(context.Background(), signed); err != nil {
|
||||
if err := f.client.SendTransaction(context.Background(), signed, bind.PrivateTxArgs{}); err != nil {
|
||||
f.lock.Unlock()
|
||||
if err = sendError(conn, err); err != nil {
|
||||
log.Warn("Failed to send transaction transmission error to client", "err", err)
|
||||
|
|
|
@ -27,8 +27,8 @@ import (
|
|||
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||
"github.com/ethereum/go-ethereum/consensus"
|
||||
"github.com/ethereum/go-ethereum/consensus/istanbul"
|
||||
istanbulCore "github.com/ethereum/go-ethereum/consensus/istanbul/core"
|
||||
"github.com/ethereum/go-ethereum/consensus/istanbul/validator"
|
||||
istanbulCore "github.com/ethereum/go-ethereum/consensus/istanbul/core"
|
||||
"github.com/ethereum/go-ethereum/core/state"
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/crypto/sha3"
|
||||
|
@ -289,8 +289,8 @@ func (sb *backend) verifyCommittedSeals(chain consensus.ChainReader, header *typ
|
|||
}
|
||||
}
|
||||
|
||||
// The length of validSeal should be larger than number of faulty node + 1
|
||||
if validSeal <= 2*snap.ValSet.F() {
|
||||
// The length of validSeal should be larger than number of faulty node + 1
|
||||
if validSeal <= snap.ValSet.F() {
|
||||
return errInvalidCommittedSeals
|
||||
}
|
||||
|
||||
|
@ -416,42 +416,45 @@ func (sb *backend) Seal(chain consensus.ChainReader, block *types.Block, results
|
|||
return err
|
||||
}
|
||||
|
||||
// wait for the timestamp of header, use this to adjust the block period
|
||||
delay := time.Unix(block.Header().Time.Int64(), 0).Sub(now())
|
||||
select {
|
||||
case <-time.After(delay):
|
||||
case <-stop:
|
||||
results <- nil
|
||||
return nil
|
||||
}
|
||||
delay := time.Unix(header.Time.Int64(), 0).Sub(now())
|
||||
|
||||
// get the proposed block hash and clear it if the seal() is completed.
|
||||
sb.sealMu.Lock()
|
||||
sb.proposedBlockHash = block.Hash()
|
||||
clear := func() {
|
||||
sb.proposedBlockHash = common.Hash{}
|
||||
sb.sealMu.Unlock()
|
||||
}
|
||||
defer clear()
|
||||
|
||||
// post block into Istanbul engine
|
||||
go sb.EventMux().Post(istanbul.RequestEvent{
|
||||
Proposal: block,
|
||||
})
|
||||
for {
|
||||
go func() {
|
||||
// wait for the timestamp of header, use this to adjust the block period
|
||||
select {
|
||||
case result := <-sb.commitCh:
|
||||
// if the block hash and the hash from channel are the same,
|
||||
// return the result. Otherwise, keep waiting the next hash.
|
||||
if result != nil && block.Hash() == result.Hash() {
|
||||
results <- result
|
||||
return nil
|
||||
}
|
||||
case <-time.After(delay):
|
||||
case <-stop:
|
||||
results <- nil
|
||||
return nil
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// get the proposed block hash and clear it if the seal() is completed.
|
||||
sb.sealMu.Lock()
|
||||
sb.proposedBlockHash = block.Hash()
|
||||
|
||||
defer func() {
|
||||
sb.proposedBlockHash = common.Hash{}
|
||||
sb.sealMu.Unlock()
|
||||
}()
|
||||
// post block into Istanbul engine
|
||||
go sb.EventMux().Post(istanbul.RequestEvent{
|
||||
Proposal: block,
|
||||
})
|
||||
for {
|
||||
select {
|
||||
case result := <-sb.commitCh:
|
||||
// if the block hash and the hash from channel are the same,
|
||||
// return the result. Otherwise, keep waiting the next hash.
|
||||
if result != nil && block.Hash() == result.Hash() {
|
||||
results <- result
|
||||
return
|
||||
}
|
||||
case <-stop:
|
||||
results <- nil
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
// update timestamp and signature of the block based on its number of transactions
|
||||
|
|
|
@ -195,30 +195,44 @@ func TestSealCommittedOtherHash(t *testing.T) {
|
|||
chain, engine := newBlockChain(4)
|
||||
block := makeBlockWithoutSeal(chain, engine, chain.Genesis())
|
||||
otherBlock := makeBlockWithoutSeal(chain, engine, block)
|
||||
expectedCommittedSeal := append([]byte{1, 2, 3}, bytes.Repeat([]byte{0x00}, types.IstanbulExtraSeal-3)...)
|
||||
|
||||
eventSub := engine.EventMux().Subscribe(istanbul.RequestEvent{})
|
||||
eventLoop := func() {
|
||||
blockOutputChannel := make(chan *types.Block)
|
||||
stopChannel := make(chan struct{})
|
||||
|
||||
go func() {
|
||||
select {
|
||||
case ev := <-eventSub.Chan():
|
||||
_, ok := ev.Data.(istanbul.RequestEvent)
|
||||
if !ok {
|
||||
if _, ok := ev.Data.(istanbul.RequestEvent); !ok {
|
||||
t.Errorf("unexpected event comes: %v", reflect.TypeOf(ev.Data))
|
||||
}
|
||||
engine.Commit(otherBlock, [][]byte{})
|
||||
if err := engine.Commit(otherBlock, [][]byte{expectedCommittedSeal}); err != nil {
|
||||
t.Error(err.Error())
|
||||
}
|
||||
}
|
||||
eventSub.Unsubscribe()
|
||||
}
|
||||
go eventLoop()
|
||||
seal := func() {
|
||||
engine.Seal(chain, block, nil, make(chan struct{}))
|
||||
t.Error("seal should not be completed")
|
||||
}
|
||||
go seal()
|
||||
}()
|
||||
|
||||
go func() {
|
||||
if err := engine.Seal(chain, block, blockOutputChannel, stopChannel); err != nil {
|
||||
t.Error(err.Error())
|
||||
}
|
||||
}()
|
||||
|
||||
const timeoutDura = 2 * time.Second
|
||||
timeout := time.NewTimer(timeoutDura)
|
||||
select {
|
||||
case <-timeout.C:
|
||||
// wait 2 seconds to ensure we cannot get any blocks from Istanbul
|
||||
case <-blockOutputChannel:
|
||||
t.Error("Wrong block found!")
|
||||
default:
|
||||
//no block found, stop the sealing
|
||||
close(stopChannel)
|
||||
}
|
||||
|
||||
select {
|
||||
case output := <-blockOutputChannel:
|
||||
if output != nil {
|
||||
t.Error("Block not nil!")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -16,6 +16,8 @@
|
|||
|
||||
package istanbul
|
||||
|
||||
import "math/big"
|
||||
|
||||
type ProposerPolicy uint64
|
||||
|
||||
const (
|
||||
|
@ -28,6 +30,7 @@ type Config struct {
|
|||
BlockPeriod uint64 `toml:",omitempty"` // Default minimum difference between two consecutive block's timestamps in second
|
||||
ProposerPolicy ProposerPolicy `toml:",omitempty"` // The policy for proposer selection
|
||||
Epoch uint64 `toml:",omitempty"` // The number of blocks after which to checkpoint and reset the pending votes
|
||||
Ceil2Nby3Block *big.Int `toml:",omitempty"` // Number of confirmations required to move from one state to next [2F + 1 to Ceil(2N/3)]
|
||||
}
|
||||
|
||||
var DefaultConfig = &Config{
|
||||
|
@ -35,4 +38,5 @@ var DefaultConfig = &Config{
|
|||
BlockPeriod: 1,
|
||||
ProposerPolicy: RoundRobin,
|
||||
Epoch: 30000,
|
||||
Ceil2Nby3Block: big.NewInt(0),
|
||||
}
|
||||
|
|
|
@ -72,7 +72,7 @@ func (c *core) handleCommit(msg *message, src istanbul.Validator) error {
|
|||
//
|
||||
// If we already have a proposal, we may have chance to speed up the consensus process
|
||||
// by committing the proposal without PREPARE messages.
|
||||
if c.current.Commits.Size() > 2*c.valSet.F() && c.state.Cmp(StateCommitted) < 0 {
|
||||
if c.current.Commits.Size() >= c.QuorumSize() && c.state.Cmp(StateCommitted) < 0 {
|
||||
// Still need to call LockHash here since state can skip Prepared state and jump directly to the Committed state.
|
||||
c.current.LockHash()
|
||||
c.commit()
|
||||
|
|
|
@ -191,8 +191,8 @@ OUTER:
|
|||
if r0.state != StatePrepared {
|
||||
t.Errorf("state mismatch: have %v, want %v", r0.state, StatePrepared)
|
||||
}
|
||||
if r0.current.Commits.Size() > 2*r0.valSet.F() {
|
||||
t.Errorf("the size of commit messages should be less than %v", 2*r0.valSet.F()+1)
|
||||
if r0.current.Commits.Size() >= r0.QuorumSize() {
|
||||
t.Errorf("the size of commit messages should be less than %v", r0.QuorumSize())
|
||||
}
|
||||
if r0.current.IsHashLocked() {
|
||||
t.Errorf("block should not be locked")
|
||||
|
@ -200,12 +200,12 @@ OUTER:
|
|||
continue
|
||||
}
|
||||
|
||||
// core should have 2F+1 prepare messages
|
||||
if r0.current.Commits.Size() <= 2*r0.valSet.F() {
|
||||
t.Errorf("the size of commit messages should be larger than 2F+1: size %v", r0.current.Commits.Size())
|
||||
// core should have 2F+1 before Ceil2Nby3Block or Ceil(2N/3) prepare messages
|
||||
if r0.current.Commits.Size() < r0.QuorumSize() {
|
||||
t.Errorf("the size of commit messages should be larger than 2F+1 or Ceil(2N/3): size %v", r0.QuorumSize())
|
||||
}
|
||||
|
||||
// check signatures large than 2F+1
|
||||
// check signatures large than F
|
||||
signedCount := 0
|
||||
committedSeals := v0.committedMsgs[0].committedSeals
|
||||
for _, validator := range r0.valSet.List() {
|
||||
|
@ -216,8 +216,8 @@ OUTER:
|
|||
}
|
||||
}
|
||||
}
|
||||
if signedCount <= 2*r0.valSet.F() {
|
||||
t.Errorf("the expected signed count should be larger than %v, but got %v", 2*r0.valSet.F(), signedCount)
|
||||
if signedCount <= r0.valSet.F() {
|
||||
t.Errorf("the expected signed count should be larger than %v, but got %v", r0.valSet.F(), signedCount)
|
||||
}
|
||||
if !r0.current.IsHashLocked() {
|
||||
t.Errorf("block should be locked")
|
||||
|
|
|
@ -342,6 +342,15 @@ func (c *core) checkValidatorSignature(data []byte, sig []byte) (common.Address,
|
|||
return istanbul.CheckValidatorSignature(c.valSet, data, sig)
|
||||
}
|
||||
|
||||
func (c *core) QuorumSize() int {
|
||||
if c.config.Ceil2Nby3Block == nil || (c.current != nil && c.current.sequence.Cmp(c.config.Ceil2Nby3Block) < 0) {
|
||||
c.logger.Trace("Confirmation Formula used 2F+ 1")
|
||||
return (2 * c.valSet.F()) + 1
|
||||
}
|
||||
c.logger.Trace("Confirmation Formula used ceil(2N/3)")
|
||||
return int(math.Ceil(float64(2*c.valSet.Size()) / 3))
|
||||
}
|
||||
|
||||
// PrepareCommittedSeal returns a committed seal for the given hash
|
||||
func PrepareCommittedSeal(hash common.Hash) []byte {
|
||||
var buf bytes.Buffer
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
package core
|
||||
|
||||
import (
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"math/big"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
@ -80,3 +81,20 @@ func TestNewRequest(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuorumSize(t *testing.T) {
|
||||
N := uint64(4)
|
||||
F := uint64(1)
|
||||
|
||||
sys := NewTestSystemWithBackend(N, F)
|
||||
backend := sys.backends[0]
|
||||
c := backend.engine.(*core)
|
||||
|
||||
valSet := c.valSet
|
||||
for i := 1; i <= 1000; i++ {
|
||||
valSet.AddValidator(common.StringToAddress(string(i)))
|
||||
if 2*c.QuorumSize() <= (valSet.Size()+valSet.F()) || 2*c.QuorumSize() > (valSet.Size()+valSet.F()+2) {
|
||||
t.Errorf("quorumSize constraint failed, expected value (2*QuorumSize > Size+F && 2*QuorumSize <= Size+F+2) to be:%v, got: %v, for size: %v", true, false, valSet.Size())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -59,7 +59,7 @@ func (c *core) handlePrepare(msg *message, src istanbul.Validator) error {
|
|||
|
||||
// Change to Prepared state if we've received enough PREPARE messages or it is locked
|
||||
// and we are in earlier state before Prepared state.
|
||||
if ((c.current.IsHashLocked() && prepare.Digest == c.current.GetLockedHash()) || c.current.GetPrepareOrCommitSize() > 2*c.valSet.F()) &&
|
||||
if ((c.current.IsHashLocked() && prepare.Digest == c.current.GetLockedHash()) || c.current.GetPrepareOrCommitSize() >= c.QuorumSize()) &&
|
||||
c.state.Cmp(StatePrepared) < 0 {
|
||||
c.current.LockHash()
|
||||
c.setState(StatePrepared)
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
package core
|
||||
|
||||
import (
|
||||
"math"
|
||||
"math/big"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
@ -156,12 +157,11 @@ func TestHandlePrepare(t *testing.T) {
|
|||
errInconsistentSubject,
|
||||
},
|
||||
{
|
||||
// less than 2F+1
|
||||
func() *testSystem {
|
||||
sys := NewTestSystemWithBackend(N, F)
|
||||
|
||||
// save less than 2*F+1 replica
|
||||
sys.backends = sys.backends[2*int(F)+1:]
|
||||
// save less than Ceil(2*N/3) replica
|
||||
sys.backends = sys.backends[int(math.Ceil(float64(2*N)/3)):]
|
||||
|
||||
for i, backend := range sys.backends {
|
||||
c := backend.engine.(*core)
|
||||
|
@ -214,8 +214,8 @@ OUTER:
|
|||
if r0.state != StatePreprepared {
|
||||
t.Errorf("state mismatch: have %v, want %v", r0.state, StatePreprepared)
|
||||
}
|
||||
if r0.current.Prepares.Size() > 2*r0.valSet.F() {
|
||||
t.Errorf("the size of PREPARE messages should be less than %v", 2*r0.valSet.F()+1)
|
||||
if r0.current.Prepares.Size() >= r0.QuorumSize() {
|
||||
t.Errorf("the size of PREPARE messages should be less than %v", r0.QuorumSize())
|
||||
}
|
||||
if r0.current.IsHashLocked() {
|
||||
t.Errorf("block should not be locked")
|
||||
|
@ -224,12 +224,12 @@ OUTER:
|
|||
continue
|
||||
}
|
||||
|
||||
// core should have 2F+1 PREPARE messages
|
||||
if r0.current.Prepares.Size() <= 2*r0.valSet.F() {
|
||||
t.Errorf("the size of PREPARE messages should be larger than 2F+1: size %v", r0.current.Commits.Size())
|
||||
// core should have 2F+1 before Ceil2Nby3Block and Ceil(2N/3) after Ceil2Nby3Block PREPARE messages
|
||||
if r0.current.Prepares.Size() < r0.QuorumSize() {
|
||||
t.Errorf("the size of PREPARE messages should be larger than 2F+1 or ceil(2N/3): size %v", r0.current.Commits.Size())
|
||||
}
|
||||
|
||||
// a message will be delivered to backend if 2F+1
|
||||
// a message will be delivered to backend if ceil(2N/3)
|
||||
if int64(len(v0.sentMsgs)) != 1 {
|
||||
t.Errorf("the Send() should be called once: times %v", len(test.system.backends[0].sentMsgs))
|
||||
}
|
||||
|
|
|
@ -82,9 +82,9 @@ func (c *core) handlePreprepare(msg *message, src istanbul.Validator) error {
|
|||
|
||||
// Verify the proposal we received
|
||||
if duration, err := c.backend.Verify(preprepare.Proposal); err != nil {
|
||||
logger.Warn("Failed to verify proposal", "err", err, "duration", duration)
|
||||
// if it's a future block, we will handle it again after the duration
|
||||
if err == consensus.ErrFutureBlock {
|
||||
logger.Info("Proposed block will be handled in the future", "err", err, "duration", duration)
|
||||
c.stopFuturePreprepareTimer()
|
||||
c.futurePreprepareTimer = time.AfterFunc(duration, func() {
|
||||
c.sendEvent(backlogEvent{
|
||||
|
@ -93,6 +93,7 @@ func (c *core) handlePreprepare(msg *message, src istanbul.Validator) error {
|
|||
})
|
||||
})
|
||||
} else {
|
||||
logger.Warn("Failed to verify proposal", "err", err, "duration", duration)
|
||||
c.sendNextRoundChange()
|
||||
}
|
||||
return err
|
||||
|
|
|
@ -98,8 +98,8 @@ func (c *core) handleRoundChange(msg *message, src istanbul.Validator) error {
|
|||
c.sendRoundChange(roundView.Round)
|
||||
}
|
||||
return nil
|
||||
} else if num == int(2*c.valSet.F()+1) && (c.waitingForRoundChange || cv.Round.Cmp(roundView.Round) < 0) {
|
||||
// We've received 2f+1 ROUND CHANGE messages, start a new round immediately.
|
||||
} else if num == c.QuorumSize() && (c.waitingForRoundChange || cv.Round.Cmp(roundView.Round) < 0) {
|
||||
// We've received 2f+1/Ceil(2N/3) ROUND CHANGE messages, start a new round immediately.
|
||||
c.startNewRound(roundView.Round)
|
||||
return nil
|
||||
} else if cv.Round.Cmp(roundView.Round) < 0 {
|
||||
|
|
|
@ -2,13 +2,14 @@
|
|||
|
||||
## Introduction
|
||||
|
||||
The link attached holds an implementation of a [Raft](https://raft.github.io)-based consensus mechanism (using [etcd](https://github.com/coreos/etcd)'s [Raft implementation](https://github.com/coreos/etcd/tree/master/raft)) as an alternative to Ethereum's default proof-of-work. This is useful for closed-membership/consortium settings where byzantine fault tolerance is not a requirement, and there is a desire for faster blocktimes (on the order of milliseconds instead of seconds) and transaction finality (the absence of forking.) Also, compared with QuorumChain, this consensus mechanism does not "unnecessarily" create empty blocks, and effectively creates blocks "on-demand."
|
||||
Quorum includes an implementation of a [Raft](https://raft.github.io)-based consensus mechanism (using [etcd](https://github.com/coreos/etcd)'s [Raft implementation](https://github.com/coreos/etcd/tree/master/raft)) as an alternative to Ethereum's default proof-of-work. This is useful for closed-membership/consortium settings where byzantine fault tolerance is not a requirement, and there is a desire for faster blocktimes (on the order of milliseconds instead of seconds) and transaction finality (the absence of forking.) This consensus mechanism does not "unnecessarily" create empty blocks, and effectively creates blocks "on-demand."
|
||||
|
||||
When the `geth` binary is passed the `--raft` flag, the node will operate in "raft mode."
|
||||
|
||||
## Some implementation basics
|
||||
|
||||
Note: Though we use the etcd implementation of the Raft protocol, we speak of "Raft" more broadly to refer to the Raft protocol, and its use to achieve consensus for Quorum/Ethereum.
|
||||
!!! note
|
||||
Though we use the etcd implementation of the Raft protocol, we speak of "Raft" more broadly to refer to the Raft protocol, and its use to achieve consensus for Quorum/Ethereum.
|
||||
|
||||
Both Raft and Ethereum have their own notion of a "node":
|
||||
|
||||
|
@ -25,7 +26,7 @@ verifier | follower
|
|||
|
||||
The main reasons we co-locate the leader and minter are (1) convenience, in that Raft ensures there is only one leader at a time, and (2) to avoid a network hop from a node minting blocks to the leader, through which all Raft writes must flow. Our implementation watches Raft leadership changes -- if a node becomes a leader it will start minting, and if a node loses its leadership, it will stop minting.
|
||||
|
||||
An observant reader might note that during raft leadership transitions, there could be a small period of time where more than one node might assume that it has minting duties; we detail how correctness is preserved in more detail later in this document.
|
||||
An observant reader might note that during raft leadership transitions, there could be a small period of time where more than one node might assume that it has minting duties; we detail how correctness is preserved in the [Chain extension, races, and correctness](#chain-extension-races-and-correctness) section.
|
||||
|
||||
We use the existing Ethereum p2p transport layer to communicate transactions between nodes, but we communicate blocks only through the Raft transport layer. They are created by the minter and flow from there to the rest of the cluster, always in the same order, via Raft.
|
||||
|
||||
|
@ -45,7 +46,7 @@ Let's follow the lifecycle of a typical transaction:
|
|||
#### on the minter:
|
||||
|
||||
3. It reaches the minter, where it's included in the next block (see `mintNewBlock`) via the transaction pool.
|
||||
4. Block creation triggers a [`NewMinedBlockEvent`](https://godoc.org/github.com/jpmorganchase/quorum/core#NewMinedBlockEvent), which the Raft protocol manager receives via its subscription `minedBlockSub`. The `minedBroadcastLoop` (in raft/handler.go) puts this new block to the `ProtocolManager.blockProposalC` channel.
|
||||
4. Block creation triggers a [`NewMinedBlockEvent`](https://godoc.org/github.com/jpmorganchase/quorum/core#NewMinedBlockEvent), which the Raft protocol manager receives via its subscription `minedBlockSub`. The `minedBroadcastLoop` (in `raft/handler.go`) puts this new block to the `ProtocolManager.blockProposalC` channel.
|
||||
5. `serveLocalProposals` is waiting at the other end of the channel. Its job is to RLP-encode blocks and propose them to Raft. Once it flows through Raft, this block will likely become the new head of the blockchain (on all nodes.)
|
||||
|
||||
#### on every node:
|
||||
|
@ -57,9 +58,9 @@ Let's follow the lifecycle of a typical transaction:
|
|||
8. The block is now handled by `applyNewChainHead`. This method checks whether the block extends the chain (i.e. it's parent is the current head of the chain; see below). If it does not extend the chain, it is simply ignored as a no-op. If it does extend chain, the block is validated and then written as the new head of the chain by [`InsertChain`](https://godoc.org/github.com/jpmorganchase/quorum/core#BlockChain.InsertChain).
|
||||
|
||||
9. A [`ChainHeadEvent`](https://godoc.org/github.com/jpmorganchase/quorum/core#ChainHeadEvent) is posted to notify listeners that a new block has been accepted. This is relevant to us because:
|
||||
* It removes the relevant transaction from the transaction pool.
|
||||
* It removes the relevant transaction from `speculativeChain`'s `proposedTxes` (see below).
|
||||
* It triggers `requestMinting` in (minter.go), telling the node to schedule the minting of a new block if any more transactions are pending.
|
||||
* It removes the relevant transaction from the transaction pool.
|
||||
* It removes the relevant transaction from `speculativeChain`'s `proposedTxes` (see below).
|
||||
* It triggers `requestMinting` in (`minter.go`), telling the node to schedule the minting of a new block if any more transactions are pending.
|
||||
|
||||
The transaction is now available on all nodes in the cluster with complete finality. Because Raft guarantees a single ordering of entries stored in its log, and because everything that is committed is guaranteed to remain so, there is no forking of the blockchain built upon Raft.
|
||||
|
||||
|
@ -118,13 +119,13 @@ This default of 50ms is configurable via the `--raftblocktime` flag to geth.
|
|||
|
||||
One of the ways our approach differs from vanilla Ethereum is that we introduce a new concept of "speculative minting." This is not strictly required for the core functionality of Raft-based Ethereum consensus, but rather it is an optimization that affords lower latency between blocks (or: faster transaction "finality.")
|
||||
|
||||
It takes some time for a block to flow through Raft (consensus) to become the head of the chain. If we synchronously waited for a block to become the new head of the chain before creating the new block, any transactions that we receive would take more time to make it into the chain.
|
||||
It takes some time for a block to flow through Raft (consensus) and become the head of the chain. If we synchronously waited for a block to become the new head of the chain before creating the new block, any transactions that we receive would take more time to make it into the chain.
|
||||
|
||||
In speculative minting we allow the creation of a new block (and its proposal to Raft) before its parent has made it all the way through Raft and into the blockchain.
|
||||
|
||||
Since this can happen repeatedly, these blocks (which each have a reference to their parent block) can form a sort of chain. We call this a "speculative chain."
|
||||
|
||||
During the course of operation that a speculative chain forms, we keep track of the subset of transactions in the pool that we have already put into blocks (in the speculative chain) that have not yet made it into the blockchain (and whereupon a [`core.ChainHeadEvent`](https://godoc.org/github.com/jpmorganchase/quorum/core#ChainHeadEvent) occurs.) These are called "proposed transactions" (see speculative_chain.go).
|
||||
During the course of operation that a speculative chain forms, we keep track of the subset of transactions in the pool that we have already put into blocks (in the speculative chain) that have not yet made it into the blockchain (and whereupon a [`core.ChainHeadEvent`](https://godoc.org/github.com/jpmorganchase/quorum/core#ChainHeadEvent) occurs.) These are called "proposed transactions" (see `speculative_chain.go`).
|
||||
|
||||
Per the presence of "races" (as we detail above), it is possible that a block somewhere in the middle of a speculative chain ends up not making into the chain. In this scenario an [`InvalidRaftOrdering`](https://godoc.org/github.com/jpmorganchase/quorum/raft#InvalidRaftOrdering) event will occur, and we clean up the state of the speculative chain accordingly.
|
||||
|
||||
|
@ -135,14 +136,14 @@ There is currently no limit to the length of these speculative chains, but we pl
|
|||
* `head`: The last-created speculative block. This can be `nil` if the last-created block is already included in the blockchain.
|
||||
* `proposedTxes`: The set of transactions which have been proposed to Raft in some block, but not yet included in the blockchain.
|
||||
* `unappliedBlocks`: A queue of blocks which have been proposed to Raft but not yet committed to the blockchain.
|
||||
- When minting a new block, we enqueue it at the end of this queue
|
||||
- `accept` is called to remove the oldest speculative block when it's accepted into the blockchain.
|
||||
- When an [`InvalidRaftOrdering`](https://godoc.org/github.com/jpmorganchase/quorum/raft#InvalidRaftOrdering) occurs, we unwind the queue by popping the most recent blocks from the "new end" of the queue until we find the invalid block. We must repeatedly remove these "newer" speculative blocks because they are all dependent on a block that we know has not been included in the blockchain.
|
||||
- When minting a new block, we enqueue it at the end of this queue
|
||||
- `accept` is called to remove the oldest speculative block when it's accepted into the blockchain.
|
||||
- When an [`InvalidRaftOrdering`](https://godoc.org/github.com/jpmorganchase/quorum/raft#InvalidRaftOrdering) occurs, we unwind the queue by popping the most recent blocks from the "new end" of the queue until we find the invalid block. We must repeatedly remove these "newer" speculative blocks because they are all dependent on a block that we know has not been included in the blockchain.
|
||||
* `expectedInvalidBlockHashes`: The set of blocks which build on an invalid block, but haven't passsed through Raft yet. We remove these as we get them back. When these non-extending blocks come back through Raft we remove them from the speculative chain. We use this set as a "guard" against trying to trim the speculative chain when we shouldn't.
|
||||
|
||||
## The Raft transport layer
|
||||
|
||||
We communicate blocks over the HTTP transport layer built in to etcd Raft. It's also (at least theoretically) possible to use p2p protocol built-in to Ethereum as a transport for Raft. In our testing we found the default etcd HTTP transport to be more reliable than the p2p (at least as implemented in geth) under high load.
|
||||
We communicate blocks over the HTTP transport layer built in to etcd Raft. It's also (at least theoretically) possible to use the p2p protocol built-in to Ethereum as a transport for Raft. In our testing we found the default etcd HTTP transport to be more reliable than the p2p (at least as implemented in geth) under high load.
|
||||
|
||||
Quorum listens on port 50400 by default for the raft transport, but this is configurable with the `--raftport` flag.
|
||||
|
||||
|
|
|
@ -60,7 +60,7 @@
|
|||
* It saves one network call communicating the block to the leader.
|
||||
* It provides a simple way to choose a minter. If we didn't use the Raft leader we'd have to build in "minter election" at a higher level.
|
||||
|
||||
Additionally there could even be multiple minters running at the same time, but this would produce contention for which blocks actually extend the chain, reducing the productivity of the cluster (see "races" above).
|
||||
Additionally there could even be multiple minters running at the same time, but this would produce contention for which blocks actually extend the chain, reducing the productivity of the cluster (see [Raft: Chain extension, races, and correctness](../Consensus/raft/#chain-extension-races-and-correctness) above).
|
||||
|
||||
??? question "I thought there were no forks in a Raft-based blockchain. What's the deal with "speculative minting"?"
|
||||
"Speculative chains" are not forks in the blockchain. They represent a series ("chain") of blocks that have been sent through Raft, after which each of the blocks may or may not actually end up being included in *the blockchain*.
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
Tessera uses cryptographic keys to provide transaction privacy.
|
||||
|
||||
You can use existing private/public key pairs as well as use Tessera to generate new key pairs for you. See [Generating & securing keys](../../Tessera%20Services/Keys/Keys) for more info.
|
||||
```
|
||||
```json
|
||||
"keys": {
|
||||
"passwords": [],
|
||||
"passwordFile": "Path",
|
||||
|
@ -16,7 +16,7 @@ You can use existing private/public key pairs as well as use Tessera to generate
|
|||
},
|
||||
"keyData": [
|
||||
{
|
||||
//The data for a private/public key pair
|
||||
// The data for a private/public key pair
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -25,11 +25,14 @@ You can use existing private/public key pairs as well as use Tessera to generate
|
|||
## KeyData
|
||||
Key pairs can be provided in several ways:
|
||||
|
||||
#### 1. Direct key pairs
|
||||
Direct key pairs are convenient but are the least secure configuration option available, as you expose your private key in the configuration file. More secure options are available and preferable for production environments.
|
||||
### Direct key pairs
|
||||
|
||||
The key pair data is provided in plain text in the configfile:
|
||||
```
|
||||
!!! warning
|
||||
Direct key pairs and unprotected inline key pairs are convenient but are the least secure configuration options available as the private key is exposed in the configuration file. The other options available are more secure and recommended for production environments.
|
||||
|
||||
The key pair data is provided in plain text in the configfile.
|
||||
|
||||
```json
|
||||
"keys": {
|
||||
"keyData": [
|
||||
{
|
||||
|
@ -40,9 +43,14 @@ The key pair data is provided in plain text in the configfile:
|
|||
}
|
||||
```
|
||||
|
||||
#### 2. Inline key pairs
|
||||
The public key is provided in plain text. The private key is provided through additional config:
|
||||
```
|
||||
### Inline key pairs
|
||||
#### Unprotected
|
||||
|
||||
!!! warning
|
||||
Direct key pairs and unprotected inline key pairs are convenient but are the least secure configuration options available as the private key is exposed in the configuration file. The other options available are more secure and recommended for production environments.
|
||||
|
||||
The key pair data is provided in plain text in the configfile. The plain text private key is provided in a `config` json object:
|
||||
```json
|
||||
"keys": {
|
||||
"keyData": [
|
||||
{
|
||||
|
@ -58,11 +66,13 @@ The public key is provided in plain text. The private key is provided through a
|
|||
}
|
||||
```
|
||||
|
||||
This allows for the use of Argon2 password-secured private keys by including the corresponding Argon2 settings in the additional config:
|
||||
#### Protected
|
||||
The public key is provided in plain text. The private key must be password-protected using Argon2. The corresponding encrypted data is provided in the `config` json object.
|
||||
|
||||
```
|
||||
```json
|
||||
"keys": {
|
||||
"passwords": ["password"],
|
||||
"passwordFile": "/path/to/pwds.txt",
|
||||
"keyData": [
|
||||
{
|
||||
"config": {
|
||||
|
@ -85,9 +95,73 @@ This allows for the use of Argon2 password-secured private keys by including the
|
|||
}
|
||||
```
|
||||
|
||||
#### 3. Azure Key Vault key pairs
|
||||
The keys in the pair are stored as secrets in an Azure Key Vault. This requires providing the vault url and the secret IDs for both keys:
|
||||
Passwords must be provided so that Tessera can decrypt and use the private keys. Passwords can be provided in multiple ways:
|
||||
|
||||
| | Description |
|
||||
|--------|--------------|
|
||||
| File | `"passwordFile": "/path/to/pwds.txt"`<br/>Must contain only one password per line. Empty lines should be used for unlocked keys. Passwords must be provided in the order that key pairs are defined in the config. |
|
||||
| Direct | `"passwords": ["pwd1", "pwd2", ...]`<br/>Empty strings should be used for unlocked keys. Passwords must be provided in the order that key pairs are defined in the config. Not recommended for production use. |
|
||||
| CLI | Tessera will prompt on the CLI for the passwords of any encrypted keys that have not had passwords provided in the config. This process only needs to be performed once, when starting the node. |
|
||||
|
||||
### Filesystem key pairs
|
||||
The keys in the pair are stored in files:
|
||||
```json
|
||||
"keys": {
|
||||
"passwords": ["password"],
|
||||
"passwordFile": "/path/to/pwds.txt",
|
||||
"keyData": [
|
||||
{
|
||||
"privateKeyPath": "/path/to/privateKey.key",
|
||||
"publicKeyPath": "/path/to/publicKey.pub"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
The contents of the public key file must contain the public key only, e.g.:
|
||||
```
|
||||
/+UuD63zItL1EbjxkKUljMgG8Z1w0AJ8pNOR4iq2yQc=
|
||||
```
|
||||
|
||||
The contents of the private key file must contain the private key in the Inline key pair format, e.g.:
|
||||
```json
|
||||
{
|
||||
"type" : "unlocked",
|
||||
"data" : {
|
||||
"bytes" : "DK0HDgMWJKtZVaP31mPhk6TJNACfVzz7VZv2PsQZeKM="
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"aopts": {
|
||||
"variant": "id",
|
||||
"memory": 1048576,
|
||||
"iterations": 10,
|
||||
"parallelism": 4,
|
||||
},
|
||||
"snonce": "x3HUNXH6LQldKtEv3q0h0hR4S12Ur9pC",
|
||||
"asalt": "7Sem2tc6fjEfW3yYUDN/kSslKEW0e1zqKnBCWbZu2Zw=",
|
||||
"sbox": "d0CmRus0rP0bdc7P7d/wnOyEW14pwFJmcLbdu2W3HmDNRWVJtoNpHrauA/Sr5Vxc"
|
||||
},
|
||||
"type": "argon2sbox"
|
||||
}
|
||||
```
|
||||
|
||||
Passwords must be provided so that Tessera can decrypt and use the private keys. Passwords can be provided in multiple ways:
|
||||
|
||||
| | Description |
|
||||
|--------|--------------|
|
||||
| File | `"passwordFile": "/path/to/pwds.txt"`<br/>Must contain only one password per line. Empty lines should be used for unlocked keys. Passwords must be provided in the order that key pairs are defined in the config. |
|
||||
| Direct | `"passwords": ["pwd1", "pwd2", ...]`<br/>Empty strings should be used for unlocked keys. Passwords must be provided in the order that key pairs are defined in the config. Not recommended for production use. |
|
||||
| CLI | Tessera will prompt on the CLI for the passwords of any encrypted keys that have not had passwords provided in the config. This process only needs to be performed once, when starting the node. |
|
||||
|
||||
### Azure Key Vault key pairs
|
||||
The keys in the pair are stored as secrets in an Azure Key Vault. This requires providing the vault url and the secret IDs for both keys:
|
||||
```json
|
||||
"keys": {
|
||||
"azureKeyVaultConfig": {
|
||||
"url": "https://my-vault.vault.azure.net"
|
||||
|
@ -105,11 +179,12 @@ The keys in the pair are stored as secrets in an Azure Key Vault. This requires
|
|||
|
||||
This example configuration will retrieve the specified versions of the secrets `Key` and `Pub` from the key vault with DNS name `https://my-vault.vault.azure.net`. If no version is specified then the latest version of the secret is retrieved.
|
||||
|
||||
> Environment variables must be set if using an Azure Key Vault, for more information see [Setting up an Azure Key Vault](../../Tessera%20Services/Keys/Setting%20up%20an%20Azure%20Key%20Vault)
|
||||
!!! info
|
||||
Environment variables must be set if using an Azure Key Vault, for more information see [Setting up an Azure Key Vault](../../Tessera%20Services/Keys/Setting%20up%20an%20Azure%20Key%20Vault)
|
||||
|
||||
#### 4. Hashicorp Vault key pairs
|
||||
### Hashicorp Vault key pairs
|
||||
The keys in the pair are stored as a secret in a Hashicorp Vault. Additional configuration can also be provided if the Vault is configured to use TLS and if the AppRole auth method is being used at a different path to the default (`approle`):
|
||||
```
|
||||
```json
|
||||
"hashicorpKeyVaultConfig": {
|
||||
"url": "https://localhost:8200",
|
||||
"tlsKeyStorePath": "/path/to/keystore.jks",
|
||||
|
@ -132,38 +207,13 @@ This example configuration will retrieve version 1 of the secret `engine/secret`
|
|||
If no `hashicorpVaultSecretVersion` is provided then the latest version for the secret will be retrieved by default.
|
||||
|
||||
Tessera requires TLS certificates and keys to be stored in `.jks` Java keystore format. If the `.jks` files are password protected then the following environment variables must be set:
|
||||
|
||||
* `HASHICORP_CLIENT_KEYSTORE_PWD`
|
||||
* `HASHICORP_CLIENT_TRUSTSTORE_PWD`
|
||||
|
||||
> If using a Hashicorp Vault additional environment variables must be set and a version 2 K/V secret engine must be enabled. For more information see [Setting up a Hashicorp Vault](../../Tessera%20Services/Keys/Setting%20up%20a%20Hashicorp%20Vault).
|
||||
!!! info
|
||||
If using a Hashicorp Vault additional environment variables must be set and a version 2 K/V secret engine must be enabled. For more information see [Setting up a Hashicorp Vault](../../Tessera%20Services/Keys/Setting%20up%20a%20Hashicorp%20Vault).
|
||||
|
||||
#### 5. Filesystem key pairs
|
||||
The keys in the pair are stored in files:
|
||||
```
|
||||
"keys": {
|
||||
"passwordFile": "/path/to/passwords",
|
||||
"keyData": [
|
||||
{
|
||||
"privateKeyPath": "/path/to/privateKey.key",
|
||||
"publicKeyPath": "/path/to/publicKey.pub"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
The contents of the public key file must contain the public key only, e.g.:
|
||||
```
|
||||
/+UuD63zItL1EbjxkKUljMgG8Z1w0AJ8pNOR4iq2yQc=
|
||||
```
|
||||
|
||||
The contents of the private key file must contain the private key in the config format, e.g.:
|
||||
```
|
||||
{
|
||||
"type" : "unlocked",
|
||||
"data" : {
|
||||
"bytes" : "DK0HDgMWJKtZVaP31mPhk6TJNACfVzz7VZv2PsQZeKM="
|
||||
}
|
||||
}
|
||||
```
|
||||
## Multiple Keys
|
||||
If wished, multiple key pairs can be specified for a Tessera node. In this case, any one of the public keys can be used to address a private transaction to that node. Tessera will sequentially try each key to find one that can decrypt the payload. This can be used, for example, to simplify key rotation.
|
||||
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
# Abigen with Quorum
|
||||
|
||||
### Overview
|
||||
|
||||
Abigen is a source code generator that converts smart contract ABI definitions into type-safe Go packages. In addition to the original capabilities provided by Ethereum described [here](https://github.com/ethereum/go-ethereum/wiki/Native-DApps:-Go-bindings-to-Ethereum-contracts). Quorum Abigen also supports private transactions.
|
||||
|
||||
### Implementation
|
||||
|
||||
`PrivateFrom` and `PrivateFor` fields have been added to the `bind.TransactOpts` which allows users to specify the public keys of the transaction manager (Tessera/Constellation) used to send and receive private transactions. The existing `ethclient` has been extended with a private transaction manager client to support sending `/storeraw` request.
|
|
@ -258,6 +258,8 @@ func CreateConsensusEngine(ctx *node.ServiceContext, chainConfig *params.ChainCo
|
|||
config.Istanbul.Epoch = chainConfig.Istanbul.Epoch
|
||||
}
|
||||
config.Istanbul.ProposerPolicy = istanbul.ProposerPolicy(chainConfig.Istanbul.ProposerPolicy)
|
||||
config.Istanbul.Ceil2Nby3Block = chainConfig.Istanbul.Ceil2Nby3Block
|
||||
|
||||
return istanbulBackend.New(&config.Istanbul, ctx.NodeKey(), db)
|
||||
}
|
||||
|
||||
|
|
|
@ -82,7 +82,7 @@ func TestNodeInfo(t *testing.T) {
|
|||
}{
|
||||
{"ethash", nil, nil, false},
|
||||
{"raft", nil, nil, true},
|
||||
{"istanbul", nil, ¶ms.IstanbulConfig{1, 1}, false},
|
||||
{"istanbul", nil, ¶ms.IstanbulConfig{1, 1, big.NewInt(0)}, false},
|
||||
{"clique", ¶ms.CliqueConfig{1, 1}, nil, false},
|
||||
}
|
||||
|
||||
|
|
|
@ -24,6 +24,8 @@ import (
|
|||
"fmt"
|
||||
"math/big"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts/abi/bind"
|
||||
|
||||
"github.com/ethereum/go-ethereum"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||
|
@ -34,7 +36,8 @@ import (
|
|||
|
||||
// Client defines typed wrappers for the Ethereum RPC API.
|
||||
type Client struct {
|
||||
c *rpc.Client
|
||||
c *rpc.Client
|
||||
pc privateTransactionManagerClient // Tessera/Constellation client
|
||||
}
|
||||
|
||||
// Dial connects a client to the given URL.
|
||||
|
@ -52,7 +55,19 @@ func DialContext(ctx context.Context, rawurl string) (*Client, error) {
|
|||
|
||||
// NewClient creates a client that uses the given RPC client.
|
||||
func NewClient(c *rpc.Client) *Client {
|
||||
return &Client{c}
|
||||
return &Client{c, nil}
|
||||
}
|
||||
|
||||
// Quorum
|
||||
//
|
||||
// provides support for private transactions
|
||||
func (ec *Client) WithPrivateTransactionManager(rawurl string) (*Client, error) {
|
||||
var err error
|
||||
ec.pc, err = newPrivateTransactionManagerClient(rawurl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ec, nil
|
||||
}
|
||||
|
||||
func (ec *Client) Close() {
|
||||
|
@ -498,12 +513,26 @@ func (ec *Client) EstimateGas(ctx context.Context, msg ethereum.CallMsg) (uint64
|
|||
//
|
||||
// If the transaction was a contract creation use the TransactionReceipt method to get the
|
||||
// contract address after the transaction has been mined.
|
||||
func (ec *Client) SendTransaction(ctx context.Context, tx *types.Transaction) error {
|
||||
func (ec *Client) SendTransaction(ctx context.Context, tx *types.Transaction, args bind.PrivateTxArgs) error {
|
||||
data, err := rlp.EncodeToBytes(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ec.c.CallContext(ctx, nil, "eth_sendRawTransaction", common.ToHex(data))
|
||||
if args.PrivateFor != nil {
|
||||
return ec.c.CallContext(ctx, nil, "eth_sendRawPrivateTransaction", common.ToHex(data), bind.PrivateTxArgs{PrivateFor: args.PrivateFor})
|
||||
} else {
|
||||
return ec.c.CallContext(ctx, nil, "eth_sendRawTransaction", common.ToHex(data))
|
||||
}
|
||||
}
|
||||
|
||||
// Quorum
|
||||
//
|
||||
// Retrieve encrypted payload hash from the private transaction manager if configured
|
||||
func (ec *Client) PreparePrivateTransaction(data []byte, privateFrom string) ([]byte, error) {
|
||||
if ec.pc == nil {
|
||||
return nil, errors.New("missing private transaction manager client configuration")
|
||||
}
|
||||
return ec.pc.storeRaw(data, privateFrom)
|
||||
}
|
||||
|
||||
func toCallArg(msg ethereum.CallMsg) interface{} {
|
||||
|
|
|
@ -22,6 +22,8 @@ import (
|
|||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/ethereum/go-ethereum"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
)
|
||||
|
@ -150,3 +152,30 @@ func TestToFilterArg(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_PreparePrivateTransaction_whenTypical(t *testing.T) {
|
||||
testObject := NewClient(nil)
|
||||
|
||||
_, err := testObject.PreparePrivateTransaction([]byte("arbitrary payload"), "arbitrary private from")
|
||||
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestClient_PreparePrivateTransaction_whenClientIsConfigured(t *testing.T) {
|
||||
expectedData := []byte("arbitrary data")
|
||||
testObject := NewClient(nil)
|
||||
testObject.pc = &privateTransactionManagerStubClient{expectedData}
|
||||
|
||||
actualData, err := testObject.PreparePrivateTransaction([]byte("arbitrary payload"), "arbitrary private from")
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, expectedData, actualData)
|
||||
}
|
||||
|
||||
type privateTransactionManagerStubClient struct {
|
||||
expectedData []byte
|
||||
}
|
||||
|
||||
func (s *privateTransactionManagerStubClient) storeRaw(data []byte, privateFrom string) ([]byte, error) {
|
||||
return s.expectedData, nil
|
||||
}
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
package ethclient
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
type privateTransactionManagerClient interface {
|
||||
storeRaw(data []byte, privateFrom string) ([]byte, error)
|
||||
}
|
||||
|
||||
type privateTransactionManagerDefaultClient struct {
|
||||
rawurl string
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
// Create a new client to interact with private transaction manager via a HTTP endpoint
|
||||
func newPrivateTransactionManagerClient(endpoint string) (privateTransactionManagerClient, error) {
|
||||
_, err := url.Parse(endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &privateTransactionManagerDefaultClient{
|
||||
rawurl: endpoint,
|
||||
httpClient: &http.Client{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
type storeRawReq struct {
|
||||
Payload string `json:"payload"`
|
||||
From string `json:"from,omitempty"`
|
||||
}
|
||||
|
||||
type storeRawResp struct {
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
func (pc *privateTransactionManagerDefaultClient) storeRaw(data []byte, privateFrom string) ([]byte, error) {
|
||||
storeRawReq := &storeRawReq{
|
||||
Payload: base64.StdEncoding.EncodeToString(data),
|
||||
From: privateFrom,
|
||||
}
|
||||
reqBodyBuf := new(bytes.Buffer)
|
||||
if err := json.NewEncoder(reqBodyBuf).Encode(storeRawReq); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := pc.httpClient.Post(pc.rawurl+"/storeraw", "application/json", reqBodyBuf)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to invoke /storeraw due to %s", err)
|
||||
}
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("server returns %s", resp.Status)
|
||||
}
|
||||
// parse response
|
||||
var storeRawResp storeRawResp
|
||||
if err := json.NewDecoder(resp.Body).Decode(&storeRawResp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
encryptedPayloadHash, err := base64.StdEncoding.DecodeString(storeRawResp.Key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return encryptedPayloadHash, nil
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
package ethclient
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const (
|
||||
arbitraryBase64Data = "YXJiaXRyYXJ5IGRhdGE=" // = "arbitrary data"
|
||||
)
|
||||
|
||||
func TestPrivateTransactionManagerClient_storeRaw(t *testing.T) {
|
||||
// mock tessera client
|
||||
arbitraryServer := newStoreRawServer()
|
||||
defer arbitraryServer.Close()
|
||||
testObject, err := newPrivateTransactionManagerClient(arbitraryServer.URL)
|
||||
assert.NoError(t, err)
|
||||
|
||||
key, err := testObject.storeRaw([]byte("arbitrary payload"), "arbitrary private from")
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "arbitrary data", string(key))
|
||||
}
|
||||
|
||||
func newStoreRawServer() *httptest.Server {
|
||||
arbitraryResponse := fmt.Sprintf(`
|
||||
{
|
||||
"key": "%s"
|
||||
}
|
||||
`, arbitraryBase64Data)
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/storeraw", func(w http.ResponseWriter, req *http.Request) {
|
||||
if req.Method == "POST" {
|
||||
// parse request
|
||||
var storeRawReq storeRawReq
|
||||
if err := json.NewDecoder(req.Body).Decode(&storeRawReq); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
// send response
|
||||
_, _ = fmt.Fprintf(w, "%s", arbitraryResponse)
|
||||
} else {
|
||||
http.Error(w, "Invalid request method", http.StatusMethodNotAllowed)
|
||||
}
|
||||
|
||||
})
|
||||
return httptest.NewServer(mux)
|
||||
}
|
|
@ -1593,7 +1593,7 @@ func (s *PublicTransactionPoolAPI) Resend(ctx context.Context, sendArgs SendTxAr
|
|||
}
|
||||
newTx := sendArgs.toTransaction()
|
||||
// set v param to 37 to indicate private tx before submitting to the signer.
|
||||
if len(sendArgs.PrivateFor) > 0 {
|
||||
if sendArgs.PrivateFor != nil {
|
||||
newTx.SetPrivate()
|
||||
}
|
||||
signedTx, err := s.sign(sendArgs.From, newTx)
|
||||
|
|
|
@ -544,7 +544,10 @@ func (w *worker) taskLoop() {
|
|||
w.pendingMu.Lock()
|
||||
w.pendingTasks[w.engine.SealHash(task.block.Header())] = task
|
||||
w.pendingMu.Unlock()
|
||||
go w.seal(task.block, stopCh)
|
||||
|
||||
if err := w.engine.Seal(w.chain, task.block, w.resultCh, stopCh); err != nil {
|
||||
log.Warn("Block sealing failed", "err", err)
|
||||
}
|
||||
case <-w.exitCh:
|
||||
interrupt()
|
||||
return
|
||||
|
@ -552,12 +555,6 @@ func (w *worker) taskLoop() {
|
|||
}
|
||||
}
|
||||
|
||||
func (w *worker) seal(b *types.Block, stop <-chan struct{}) {
|
||||
if err := w.engine.Seal(w.chain, b, w.resultCh, stop); err != nil {
|
||||
log.Warn("Block sealing failed", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
// resultLoop is a standalone goroutine to handle sealing result submitting
|
||||
// and flush relative data to the database.
|
||||
func (w *worker) resultLoop() {
|
||||
|
@ -585,39 +582,38 @@ func (w *worker) resultLoop() {
|
|||
}
|
||||
// Different block could share same sealhash, deep copy here to prevent write-write conflict.
|
||||
var logs []*types.Log
|
||||
work := w.current
|
||||
|
||||
for _, receipt := range append(work.receipts, work.privateReceipts...) {
|
||||
for _, receipt := range append(task.receipts, task.privateReceipts...) {
|
||||
// Update the block hash in all logs since it is now available and not when the
|
||||
// receipt/log of individual transactions were created.
|
||||
for _, log := range receipt.Logs {
|
||||
log.BlockHash = hash
|
||||
}
|
||||
logs = append(logs, receipt.Logs...)
|
||||
}
|
||||
|
||||
for _, log := range append(work.state.Logs(), work.privateState.Logs()...) {
|
||||
log.BlockHash = hash
|
||||
}
|
||||
|
||||
// write private transacions
|
||||
privateStateRoot, _ := work.privateState.Commit(w.config.IsEIP158(block.Number()))
|
||||
core.WritePrivateStateRoot(w.eth.ChainDb(), block.Root(), privateStateRoot)
|
||||
allReceipts := mergeReceipts(work.receipts, work.privateReceipts)
|
||||
|
||||
// Commit block and state to database.
|
||||
w.mu.Lock()
|
||||
stat, err := w.chain.WriteBlockWithState(block, allReceipts, work.state, nil)
|
||||
w.mu.Unlock()
|
||||
// write private transactions
|
||||
privateStateRoot, err := task.privateState.Commit(w.config.IsEIP158(block.Number()))
|
||||
if err != nil {
|
||||
log.Error("Failed writWriteBlockAndStating block to chain", "err", err)
|
||||
log.Error("Failed committing private state root", "err", err)
|
||||
continue
|
||||
}
|
||||
if err := core.WritePrivateStateRoot(w.eth.ChainDb(), block.Root(), privateStateRoot); err != nil {
|
||||
log.Error("Failed writing private state root", "err", err)
|
||||
continue
|
||||
}
|
||||
allReceipts := mergeReceipts(task.receipts, task.privateReceipts)
|
||||
|
||||
if err := core.WritePrivateBlockBloom(w.eth.ChainDb(), block.NumberU64(), work.privateReceipts); err != nil {
|
||||
// Commit block and state to database.
|
||||
stat, err := w.chain.WriteBlockWithState(block, allReceipts, task.state, nil)
|
||||
if err != nil {
|
||||
log.Error("Failed writing block to chain", "err", err)
|
||||
continue
|
||||
}
|
||||
if err := core.WritePrivateBlockBloom(w.eth.ChainDb(), block.NumberU64(), task.privateReceipts); err != nil {
|
||||
log.Error("Failed writing private block bloom", "err", err)
|
||||
continue
|
||||
}
|
||||
|
||||
log.Info("Successfully sealed new block", "number", block.Number(), "sealhash", sealhash, "hash", hash,
|
||||
"elapsed", common.PrettyDuration(time.Since(task.createdAt)))
|
||||
|
||||
|
@ -625,8 +621,6 @@ func (w *worker) resultLoop() {
|
|||
w.mux.Post(core.NewMinedBlockEvent{Block: block})
|
||||
|
||||
var events []interface{}
|
||||
logs = append(work.state.Logs(), work.privateState.Logs()...)
|
||||
|
||||
switch stat {
|
||||
case core.CanonStatTy:
|
||||
events = append(events, core.ChainEvent{Block: block, Hash: block.Hash(), Logs: logs})
|
||||
|
@ -1023,8 +1017,8 @@ func (w *worker) commit(uncles []*types.Header, interval func(), update bool, st
|
|||
|
||||
privateReceipts := make([]*types.Receipt, len(w.current.privateReceipts))
|
||||
for i, l := range w.current.privateReceipts {
|
||||
receipts[i] = new(types.Receipt)
|
||||
*receipts[i] = *l
|
||||
privateReceipts[i] = new(types.Receipt)
|
||||
*privateReceipts[i] = *l
|
||||
}
|
||||
|
||||
s := w.current.state.Copy()
|
||||
|
|
|
@ -21,6 +21,8 @@ package geth
|
|||
import (
|
||||
"math/big"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts/abi/bind"
|
||||
|
||||
"github.com/ethereum/go-ethereum/core/types"
|
||||
"github.com/ethereum/go-ethereum/ethclient"
|
||||
)
|
||||
|
@ -312,5 +314,5 @@ func (ec *EthereumClient) EstimateGas(ctx *Context, msg *CallMsg) (gas int64, _
|
|||
// If the transaction was a contract creation use the TransactionReceipt method to get the
|
||||
// contract address after the transaction has been mined.
|
||||
func (ec *EthereumClient) SendTransaction(ctx *Context, tx *Transaction) error {
|
||||
return ec.client.SendTransaction(ctx.context, tx.tx)
|
||||
return ec.client.SendTransaction(ctx.context, tx.tx, bind.PrivateTxArgs{})
|
||||
}
|
||||
|
|
|
@ -124,6 +124,7 @@ var (
|
|||
Istanbul: &IstanbulConfig{
|
||||
Epoch: 30000,
|
||||
ProposerPolicy: 0,
|
||||
Ceil2Nby3Block: big.NewInt(0),
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -218,8 +219,9 @@ func (c *CliqueConfig) String() string {
|
|||
|
||||
// IstanbulConfig is the consensus engine configs for Istanbul based sealing.
|
||||
type IstanbulConfig struct {
|
||||
Epoch uint64 `json:"epoch"` // Epoch length to reset votes and checkpoint
|
||||
ProposerPolicy uint64 `json:"policy"` // The policy for proposer selection
|
||||
Epoch uint64 `json:"epoch"` // Epoch length to reset votes and checkpoint
|
||||
ProposerPolicy uint64 `json:"policy"` // The policy for proposer selection
|
||||
Ceil2Nby3Block *big.Int `json:"ceil2Nby3Block,omitempty"` // Number of confirmations required to move from one state to next [2F + 1 to Ceil(2N/3)]
|
||||
}
|
||||
|
||||
// String implements the stringer interface, returning the consensus engine details.
|
||||
|
@ -386,6 +388,9 @@ func (c *ChainConfig) checkCompatible(newcfg *ChainConfig, head *big.Int, isQuor
|
|||
if isForkIncompatible(c.EWASMBlock, newcfg.EWASMBlock, head) {
|
||||
return newCompatError("ewasm fork block", c.EWASMBlock, newcfg.EWASMBlock)
|
||||
}
|
||||
if c.Istanbul != nil && newcfg.Istanbul != nil && isForkIncompatible(c.Istanbul.Ceil2Nby3Block, newcfg.Istanbul.Ceil2Nby3Block, head) {
|
||||
return newCompatError("Ceil 2N/3 fork block", c.Istanbul.Ceil2Nby3Block, newcfg.Istanbul.Ceil2Nby3Block)
|
||||
}
|
||||
if isForkIncompatible(c.QIP714Block, newcfg.QIP714Block, head) {
|
||||
return newCompatError("permissions fork block", c.QIP714Block, newcfg.QIP714Block)
|
||||
}
|
||||
|
|
|
@ -70,6 +70,23 @@ func TestCheckCompatible(t *testing.T) {
|
|||
RewindTo: 9,
|
||||
},
|
||||
},
|
||||
{
|
||||
stored: &ChainConfig{Istanbul: &IstanbulConfig{Ceil2Nby3Block: big.NewInt(10)}},
|
||||
new: &ChainConfig{Istanbul: &IstanbulConfig{Ceil2Nby3Block: big.NewInt(20)}},
|
||||
head: 4,
|
||||
wantErr: nil,
|
||||
},
|
||||
{
|
||||
stored: &ChainConfig{Istanbul: &IstanbulConfig{Ceil2Nby3Block: big.NewInt(10)}},
|
||||
new: &ChainConfig{Istanbul: &IstanbulConfig{Ceil2Nby3Block: big.NewInt(20)}},
|
||||
head: 30,
|
||||
wantErr: &ConfigCompatError{
|
||||
What: "Ceil 2N/3 fork block",
|
||||
StoredConfig: big.NewInt(10),
|
||||
NewConfig: big.NewInt(20),
|
||||
RewindTo: 9,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
|
|
|
@ -865,10 +865,6 @@ func (pm *ProtocolManager) makeInitialRaftPeers() (raftPeers []etcdRaft.Peer, pe
|
|||
return
|
||||
}
|
||||
|
||||
func sleep(duration time.Duration) {
|
||||
<-time.NewTimer(duration).C
|
||||
}
|
||||
|
||||
func blockExtendsChain(block *types.Block, chain *core.BlockChain) bool {
|
||||
return block.ParentHash() == chain.CurrentBlock().Hash()
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue