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