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 }