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 }