github.com/datachainlab/burrow@v0.25.0/execution/contexts/proposal_context.go (about)

     1  package contexts
     2  
     3  import (
     4  	"crypto/sha256"
     5  	"fmt"
     6  	"runtime/debug"
     7  	"unicode"
     8  
     9  	"github.com/hyperledger/burrow/acm/acmstate"
    10  	"github.com/hyperledger/burrow/acm/validator"
    11  	"github.com/hyperledger/burrow/crypto"
    12  	"github.com/hyperledger/burrow/execution/errors"
    13  	"github.com/hyperledger/burrow/execution/exec"
    14  	"github.com/hyperledger/burrow/execution/proposal"
    15  	"github.com/hyperledger/burrow/logging"
    16  	"github.com/hyperledger/burrow/logging/structure"
    17  	"github.com/hyperledger/burrow/txs"
    18  	"github.com/hyperledger/burrow/txs/payload"
    19  )
    20  
    21  type ProposalContext struct {
    22  	ChainID           string
    23  	ProposalThreshold uint64
    24  	StateWriter       acmstate.ReaderWriter
    25  	ValidatorSet      validator.Writer
    26  	ProposalReg       proposal.ReaderWriter
    27  	Logger            *logging.Logger
    28  	tx                *payload.ProposalTx
    29  	Contexts          map[payload.Type]Context
    30  }
    31  
    32  func (ctx *ProposalContext) Execute(txe *exec.TxExecution, p payload.Payload) error {
    33  	var ok bool
    34  	ctx.tx, ok = p.(*payload.ProposalTx)
    35  	if !ok {
    36  		return fmt.Errorf("payload must be ProposalTx, but is: %v", txe.Envelope.Tx.Payload)
    37  	}
    38  	// Validate input
    39  	inAcc, err := ctx.StateWriter.GetAccount(ctx.tx.Input.Address)
    40  	if err != nil {
    41  		return err
    42  	}
    43  
    44  	if inAcc == nil {
    45  		ctx.Logger.InfoMsg("Cannot find input account",
    46  			"tx_input", ctx.tx.Input)
    47  		return errors.ErrorCodeInvalidAddress
    48  	}
    49  
    50  	// check permission
    51  	if !hasProposalPermission(ctx.StateWriter, inAcc, ctx.Logger) {
    52  		return fmt.Errorf("account %s does not have Proposal permission", ctx.tx.Input.Address)
    53  	}
    54  
    55  	var ballot *payload.Ballot
    56  	var proposalHash []byte
    57  
    58  	if ctx.tx.Proposal == nil {
    59  		// voting for existing proposal
    60  		if ctx.tx.ProposalHash == nil || ctx.tx.ProposalHash.Size() != sha256.Size {
    61  			return errors.ErrorCodeInvalidProposal
    62  		}
    63  
    64  		proposalHash = ctx.tx.ProposalHash.Bytes()
    65  		ballot, err = ctx.ProposalReg.GetProposal(proposalHash)
    66  		if err != nil {
    67  			return err
    68  		}
    69  	} else {
    70  		if ctx.tx.ProposalHash != nil || ctx.tx.Proposal.BatchTx == nil ||
    71  			len(ctx.tx.Proposal.BatchTx.Txs) == 0 || len(ctx.tx.Proposal.BatchTx.GetInputs()) == 0 {
    72  			return errors.ErrorCodeInvalidProposal
    73  		}
    74  
    75  		// validate the input strings
    76  		if err := validateProposalStrings(ctx.tx.Proposal); err != nil {
    77  			return err
    78  		}
    79  
    80  		proposalHash = ctx.tx.Proposal.Hash()
    81  
    82  		ballot, err = ctx.ProposalReg.GetProposal(proposalHash)
    83  		if err != nil {
    84  			return err
    85  		}
    86  
    87  		if ballot == nil {
    88  			ballot = &payload.Ballot{
    89  				Proposal:      ctx.tx.Proposal,
    90  				ProposalState: payload.Ballot_PROPOSED,
    91  			}
    92  		}
    93  
    94  		// else vote for existing proposal
    95  	}
    96  
    97  	// Check that we have not voted this already
    98  	for _, vote := range ballot.Votes {
    99  		for _, i := range ctx.tx.GetInputs() {
   100  			if i.Address == vote.Address {
   101  				return errors.ErrorCodeAlreadyVoted
   102  			}
   103  		}
   104  	}
   105  
   106  	// count votes for proposal
   107  	votes := make(map[crypto.Address]int64)
   108  
   109  	if ballot.Votes == nil {
   110  		ballot.Votes = make([]*payload.Vote, 0)
   111  	}
   112  
   113  	for _, v := range ballot.Votes {
   114  		acc, err := ctx.StateWriter.GetAccount(v.Address)
   115  		if err != nil {
   116  			return err
   117  		}
   118  		// Belt and braces, should have already been checked
   119  		if !hasProposalPermission(ctx.StateWriter, acc, ctx.Logger) {
   120  			return fmt.Errorf("account %s does not have Proposal permission", ctx.tx.Input.Address)
   121  		}
   122  		votes[v.Address] = v.VotingWeight
   123  	}
   124  
   125  	for _, i := range ballot.Proposal.BatchTx.GetInputs() {
   126  		// Validate input
   127  		proposeAcc, err := ctx.StateWriter.GetAccount(i.Address)
   128  		if err != nil {
   129  			return err
   130  		}
   131  
   132  		if proposeAcc == nil {
   133  			ctx.Logger.InfoMsg("Cannot find input account",
   134  				"tx_input", ctx.tx.Input)
   135  			return errors.ErrorCodeInvalidAddress
   136  		}
   137  
   138  		if !hasBatchPermission(ctx.StateWriter, proposeAcc, ctx.Logger) {
   139  			return fmt.Errorf("account %s does not have batch permission", i.Address)
   140  		}
   141  
   142  		if proposeAcc.GetSequence()+1 != i.Sequence {
   143  			return fmt.Errorf("proposal expired, sequence number for account %s wrong", i.Address)
   144  		}
   145  	}
   146  
   147  	for _, i := range ctx.tx.GetInputs() {
   148  		// Do we have a record of our own vote
   149  		if _, ok := votes[i.Address]; !ok {
   150  			votes[i.Address] = ctx.tx.VotingWeight
   151  			ballot.Votes = append(ballot.Votes, &payload.Vote{Address: i.Address, VotingWeight: ctx.tx.VotingWeight})
   152  		}
   153  	}
   154  
   155  	// Count the number of validators; ensure we have at least half the number of validators
   156  	// This also means that when running with a single validator, a proposal will run straight away
   157  	var power uint64
   158  	for _, v := range votes {
   159  		if v > 0 {
   160  			power++
   161  		}
   162  	}
   163  
   164  	stateCache := acmstate.NewCache(ctx.StateWriter)
   165  
   166  	for i, step := range ballot.Proposal.BatchTx.Txs {
   167  		txEnv := txs.EnvelopeFromAny(ctx.ChainID, step)
   168  
   169  		for _, input := range txEnv.Tx.GetInputs() {
   170  			acc, err := stateCache.GetAccount(input.Address)
   171  			if err != nil {
   172  				return err
   173  			}
   174  
   175  			acc.Sequence++
   176  
   177  			if acc.Sequence != input.Sequence {
   178  				return fmt.Errorf("proposal expired, sequence number %d for account %s wrong at step %d", input.Sequence, input.Address, i+1)
   179  			}
   180  
   181  			stateCache.UpdateAccount(acc)
   182  		}
   183  	}
   184  
   185  	if power >= ctx.ProposalThreshold {
   186  		ballot.ProposalState = payload.Ballot_EXECUTED
   187  
   188  		txe.TxExecutions = make([]*exec.TxExecution, 0)
   189  
   190  		for i, step := range ballot.Proposal.BatchTx.Txs {
   191  			txEnv := txs.EnvelopeFromAny(ctx.ChainID, step)
   192  
   193  			containedTxe := exec.NewTxExecution(txEnv)
   194  
   195  			defer func() {
   196  				if r := recover(); r != nil {
   197  					err = fmt.Errorf("recovered from panic in executor.Execute(%s): %v\n%s", txEnv.String(), r,
   198  						debug.Stack())
   199  				}
   200  			}()
   201  
   202  			for _, input := range txEnv.Tx.GetInputs() {
   203  				acc, err := ctx.StateWriter.GetAccount(input.Address)
   204  				if err != nil {
   205  					return err
   206  				}
   207  
   208  				acc.Sequence++
   209  
   210  				if input.Address != acc.GetAddress() {
   211  					return fmt.Errorf("trying to validate input from address %v but passed account %v", input.Address,
   212  						acc.GetAddress())
   213  				}
   214  
   215  				if acc.Sequence != input.Sequence {
   216  					return fmt.Errorf("proposal expired, sequence number %d for account %s wrong at step %d", input.Sequence, input.Address, i+1)
   217  				}
   218  
   219  				ctx.StateWriter.UpdateAccount(acc)
   220  			}
   221  
   222  			if txExecutor, ok := ctx.Contexts[txEnv.Tx.Type()]; ok {
   223  				err = txExecutor.Execute(containedTxe, txEnv.Tx.Payload)
   224  
   225  				if err != nil {
   226  					ctx.Logger.InfoMsg("Transaction execution failed", structure.ErrorKey, err)
   227  					return err
   228  				}
   229  			}
   230  
   231  			txe.TxExecutions = append(txe.TxExecutions, containedTxe)
   232  
   233  			if containedTxe.Exception != nil {
   234  				ballot.ProposalState = payload.Ballot_FAILED
   235  				break
   236  			}
   237  		}
   238  	}
   239  
   240  	return ctx.ProposalReg.UpdateProposal(proposalHash, ballot)
   241  }
   242  
   243  func validateProposalStrings(proposal *payload.Proposal) error {
   244  	if len(proposal.Name) == 0 {
   245  		return errors.ErrorCodef(errors.ErrorCodeInvalidString, "name must not be empty")
   246  	}
   247  
   248  	if !validateNameRegEntryName(proposal.Name) {
   249  		return errors.ErrorCodef(errors.ErrorCodeInvalidString,
   250  			"Invalid characters found in Proposal.Name (%s). Only alphanumeric, underscores, dashes, forward slashes, and @ are allowed", proposal.Name)
   251  	}
   252  
   253  	if !validateStringPrintable(proposal.Description) {
   254  		return errors.ErrorCodef(errors.ErrorCodeInvalidString,
   255  			"Invalid characters found in Proposal.Description (%s). Only printable characters are allowed", proposal.Description)
   256  	}
   257  
   258  	return nil
   259  }
   260  
   261  func validateStringPrintable(data string) bool {
   262  	for _, r := range []rune(data) {
   263  		if !unicode.IsPrint(r) {
   264  			return false
   265  		}
   266  	}
   267  	return true
   268  }