Michelson: forbid internal operation replay

This commit is contained in:
Benjamin Canou 2018-05-04 17:01:47 +02:00 committed by Grégoire Henry
parent f1fc7ab582
commit 439435bd11
11 changed files with 109 additions and 12 deletions

View File

@ -0,0 +1,6 @@
parameter unit ;
storage unit ;
code { CDR ; NIL operation ;
SELF ; PUSH mutez 0 ; UNIT ; TRANSFER_TOKENS ;
DUP ; DIP { CONS } ; CONS ;
PAIR }

View File

@ -385,6 +385,10 @@ bake_after $client transfer 100 from bootstrap1 to transfer_to \
assert_balance test_transfer_account2 "120 ꜩ" # Why isn't this 120 ꜩ? Baking fee? assert_balance test_transfer_account2 "120 ꜩ" # Why isn't this 120 ꜩ? Baking fee?
# Test replay prevention
init_with_transfer $contract_dir/replay.tz $key2 Unit 0 bootstrap1
assert_fails $client transfer 0 from bootstrap1 to replay
# Tests create_account # Tests create_account
init_with_transfer $contract_dir/create_account.tz $key2 None 1,000 bootstrap1 init_with_transfer $contract_dir/create_account.tz $key2 None 1,000 bootstrap1
$client transfer 100 from bootstrap1 to create_account \ $client transfer 100 from bootstrap1 to create_account \

View File

@ -209,6 +209,12 @@ let report_errors ~details ~show_source ?parsed ppf errs =
print_source (parsed, hilights) ; print_source (parsed, hilights) ;
if rest <> [] then Format.fprintf ppf "@," ; if rest <> [] then Format.fprintf ppf "@," ;
print_trace (parsed_locations parsed) rest print_trace (parsed_locations parsed) rest
| Alpha_environment.Ecoproto_error (Apply.Internal_operation_replay op) :: rest ->
Format.fprintf ppf
"@[<v 2>Internal operation replay attempt:@,%a@]"
Operation_result.pp_internal_operation op ;
if rest <> [] then Format.fprintf ppf "@," ;
print_trace locations rest
| Alpha_environment.Ecoproto_error Gas.Gas_limit_too_high :: rest -> | Alpha_environment.Ecoproto_error Gas.Gas_limit_too_high :: rest ->
Format.fprintf ppf Format.fprintf ppf
"Gas limit for the block is out of the protocol hard bounds." ; "Gas limit for the block is out of the protocol hard bounds." ;

View File

@ -136,6 +136,11 @@ let fork_test_chain = Raw_context.fork_test_chain
let endorsement_already_recorded = Raw_context.endorsement_already_recorded let endorsement_already_recorded = Raw_context.endorsement_already_recorded
let record_endorsement = Raw_context.record_endorsement let record_endorsement = Raw_context.record_endorsement
let reset_internal_nonce = Raw_context.reset_internal_nonce
let fresh_internal_nonce = Raw_context.fresh_internal_nonce
let record_internal_nonce = Raw_context.record_internal_nonce
let internal_nonce_already_recorded = Raw_context.internal_nonce_already_recorded
let add_fees = Raw_context.add_fees let add_fees = Raw_context.add_fees
let add_rewards = Raw_context.add_rewards let add_rewards = Raw_context.add_rewards

View File

@ -817,6 +817,7 @@ and counter = Int32.t
type internal_operation = { type internal_operation = {
source: Contract.contract ; source: Contract.contract ;
operation: manager_operation ; operation: manager_operation ;
nonce : int ;
} }
module Operation : sig module Operation : sig
@ -921,6 +922,11 @@ val fork_test_chain: context -> Protocol_hash.t -> Time.t -> context Lwt.t
val endorsement_already_recorded: context -> int -> bool val endorsement_already_recorded: context -> int -> bool
val record_endorsement: context -> int -> context val record_endorsement: context -> int -> context
val reset_internal_nonce: context -> context
val fresh_internal_nonce: context -> (context * int) tzresult
val record_internal_nonce: context -> int -> context
val internal_nonce_already_recorded: context -> int -> bool
val add_fees: context -> Tez.t -> context tzresult Lwt.t val add_fees: context -> Tez.t -> context tzresult Lwt.t
val add_rewards: context -> Tez.t -> context tzresult Lwt.t val add_rewards: context -> Tez.t -> context tzresult Lwt.t

View File

@ -17,6 +17,7 @@ type error += Duplicate_endorsement of int (* `Branch *)
type error += Bad_contract_parameter of Contract.t * Script.expr option * Script.lazy_expr option (* `Permanent *) type error += Bad_contract_parameter of Contract.t * Script.expr option * Script.lazy_expr option (* `Permanent *)
type error += Invalid_endorsement_level type error += Invalid_endorsement_level
type error += Invalid_commitment of { expected: bool } type error += Invalid_commitment of { expected: bool }
type error += Internal_operation_replay of internal_operation
type error += Invalid_double_endorsement_evidence (* `Permanent *) type error += Invalid_double_endorsement_evidence (* `Permanent *)
type error += Inconsistent_double_endorsement_evidence type error += Inconsistent_double_endorsement_evidence
@ -114,9 +115,19 @@ let () =
Format.fprintf ppf "Missing seed's nonce commitment in block header." Format.fprintf ppf "Missing seed's nonce commitment in block header."
else else
Format.fprintf ppf "Unexpected seed's nonce commitment in block header.") Format.fprintf ppf "Unexpected seed's nonce commitment in block header.")
Data_encoding.(obj1 (req "expected "bool)) Data_encoding.(obj1 (req "expected" bool))
(function Invalid_commitment { expected } -> Some expected | _ -> None) (function Invalid_commitment { expected } -> Some expected | _ -> None)
(fun expected -> Invalid_commitment { expected }) ; (fun expected -> Invalid_commitment { expected }) ;
register_error_kind
`Permanent
~id:"internal_operation_replay"
~title:"Internal operation replay"
~description:"An internal operation was emitted twice by a script"
~pp:(fun ppf { nonce } ->
Format.fprintf ppf "Internal operation %d was emitted twice by a script" nonce)
Operation.internal_operation_encoding
(function Internal_operation_replay op -> Some op | _ -> None)
(fun op -> Internal_operation_replay op) ;
register_error_kind register_error_kind
`Permanent `Permanent
~id:"block.invalid_double_endorsement_evidence" ~id:"block.invalid_double_endorsement_evidence"
@ -498,8 +509,13 @@ let apply_internal_manager_operations ctxt ~payer ops =
let rec apply ctxt applied worklist = let rec apply ctxt applied worklist =
match worklist with match worklist with
| [] -> Lwt.return (Ok (ctxt, applied)) | [] -> Lwt.return (Ok (ctxt, applied))
| { source ; operation } as op :: rest -> | { source ; operation ; nonce } as op :: rest ->
apply_manager_operation_content ctxt ~source ~payer ~internal:true operation >>= function begin if internal_nonce_already_recorded ctxt nonce then
fail (Internal_operation_replay op)
else
let ctxt = record_internal_nonce ctxt nonce in
apply_manager_operation_content ctxt ~source ~payer ~internal:true operation
end >>= function
| Error errors -> | Error errors ->
let result = Internal op, Failed errors in let result = Internal op, Failed errors in
let skipped = List.rev_map (fun op -> Internal op, Skipped) rest in let skipped = List.rev_map (fun op -> Internal op, Skipped) rest in
@ -561,6 +577,7 @@ let apply_sourced_operation ctxt pred_block operation ops =
Contract.increment_counter ctxt source >>=? fun ctxt -> Contract.increment_counter ctxt source >>=? fun ctxt ->
Contract.spend ctxt source fee >>=? fun ctxt -> Contract.spend ctxt source fee >>=? fun ctxt ->
add_fees ctxt fee >>=? fun ctxt -> add_fees ctxt fee >>=? fun ctxt ->
let ctxt = reset_internal_nonce ctxt in
Lwt.return (Gas.set_limit ctxt gas_limit) >>=? fun ctxt -> Lwt.return (Gas.set_limit ctxt gas_limit) >>=? fun ctxt ->
Lwt.return (Contract.set_storage_limit ctxt storage_limit) >>=? fun ctxt -> Lwt.return (Contract.set_storage_limit ctxt storage_limit) >>=? fun ctxt ->
apply_manager_operations ctxt source operations >>= begin function apply_manager_operations ctxt source operations >>= begin function

View File

@ -105,6 +105,7 @@ and counter = Int32.t
type internal_operation = { type internal_operation = {
source: Contract_repr.contract ; source: Contract_repr.contract ;
operation: manager_operation ; operation: manager_operation ;
nonce: int ;
} }
module Encoding = struct module Encoding = struct
@ -429,11 +430,12 @@ module Encoding = struct
let internal_operation_encoding = let internal_operation_encoding =
conv conv
(fun { source ; operation } -> (source, operation)) (fun { source ; operation ; nonce } -> ((source, nonce), operation))
(fun (source, operation) -> { source ; operation }) (fun ((source, nonce), operation) -> { source ; operation ; nonce })
(merge_objs (merge_objs
(obj1 (obj2
(req "source" Contract_repr.encoding)) (req "source" Contract_repr.encoding)
(req "nonce" uint16))
(union ~tag_size:`Uint8 [ (union ~tag_size:`Uint8 [
reveal_case (Tag 0) ; reveal_case (Tag 0) ;
transaction_case (Tag 1) ; transaction_case (Tag 1) ;

View File

@ -135,6 +135,7 @@ val unsigned_operation_encoding:
type internal_operation = { type internal_operation = {
source: Contract_repr.contract ; source: Contract_repr.contract ;
operation: manager_operation ; operation: manager_operation ;
nonce: int ;
} }
val internal_operation_encoding: val internal_operation_encoding:

View File

@ -16,7 +16,7 @@ type t = {
level: Level_repr.t ; level: Level_repr.t ;
timestamp: Time.t ; timestamp: Time.t ;
fitness: Int64.t ; fitness: Int64.t ;
endorsements_received: Int_set.t; endorsements_received: Int_set.t ;
fees: Tez_repr.t ; fees: Tez_repr.t ;
rewards: Tez_repr.t ; rewards: Tez_repr.t ;
block_gas: Z.t ; block_gas: Z.t ;
@ -24,6 +24,8 @@ type t = {
block_storage: Int64.t ; block_storage: Int64.t ;
operation_storage: Storage_limit_repr.t ; operation_storage: Storage_limit_repr.t ;
origination_nonce: Contract_repr.origination_nonce option ; origination_nonce: Contract_repr.origination_nonce option ;
internal_nonce: int ;
internal_nonces_used: Int_set.t ;
} }
type context = t type context = t
@ -39,6 +41,33 @@ let recover ctxt = ctxt.context
let record_endorsement ctxt k = { ctxt with endorsements_received = Int_set.add k ctxt.endorsements_received } let record_endorsement ctxt k = { ctxt with endorsements_received = Int_set.add k ctxt.endorsements_received }
let endorsement_already_recorded ctxt k = Int_set.mem k ctxt.endorsements_received let endorsement_already_recorded ctxt k = Int_set.mem k ctxt.endorsements_received
type error += Too_many_internal_operations (* `Permanent *)
let () =
let open Data_encoding in
register_error_kind
`Permanent
~id:"too_many_internal_operations"
~title: "Too many internal operations"
~description:
"A transaction exceeded the hard limit \
of internal operations it can emit"
empty
(function Too_many_internal_operations -> Some () | _ -> None)
(fun () -> Too_many_internal_operations)
let fresh_internal_nonce ctxt =
if Compare.Int.(ctxt.internal_nonce >= 65_535) then
error Too_many_internal_operations
else
ok ({ ctxt with internal_nonce = ctxt.internal_nonce + 1 }, ctxt.internal_nonce)
let reset_internal_nonce ctxt =
{ ctxt with internal_nonces_used = Int_set.empty ; internal_nonce = 0 }
let record_internal_nonce ctxt k =
{ ctxt with internal_nonces_used = Int_set.add k ctxt.internal_nonces_used }
let internal_nonce_already_recorded ctxt k =
Int_set.mem k ctxt.internal_nonces_used
let set_current_fitness ctxt fitness = { ctxt with fitness } let set_current_fitness ctxt fitness = { ctxt with fitness }
let add_fees ctxt fees = let add_fees ctxt fees =
@ -362,6 +391,8 @@ let prepare ~level ~timestamp ~fitness ctxt =
operation_storage = Unaccounted ; operation_storage = Unaccounted ;
block_storage = constants.Constants_repr.hard_storage_limit_per_block ; block_storage = constants.Constants_repr.hard_storage_limit_per_block ;
origination_nonce = None ; origination_nonce = None ;
internal_nonce = 0 ;
internal_nonces_used = Int_set.empty ;
} }
let check_first_block ctxt = let check_first_block ctxt =
@ -411,6 +442,8 @@ let register_resolvers enc resolve =
block_storage = Constants_repr.default.hard_storage_limit_per_block ; block_storage = Constants_repr.default.hard_storage_limit_per_block ;
operation_storage = Unaccounted ; operation_storage = Unaccounted ;
origination_nonce = None ; origination_nonce = None ;
internal_nonce = 0 ;
internal_nonces_used = Int_set.empty ;
} in } in
resolve faked_context str in resolve faked_context str in
Context.register_resolver enc resolve Context.register_resolver enc resolve

View File

@ -188,3 +188,16 @@ include T with type t := t and type context := context
val record_endorsement: context -> int -> context val record_endorsement: context -> int -> context
val endorsement_already_recorded: context -> int -> bool val endorsement_already_recorded: context -> int -> bool
(** Initialize the local nonce used for preventing a script to
duplicate an internal operation to replay it. *)
val reset_internal_nonce: context -> context
(** Increments the internal operation nonce. *)
val fresh_internal_nonce: context -> (context * int) tzresult
(** Mark an internal operation nonce as taken. *)
val record_internal_nonce: context -> int -> context
(** Check is the internal operation nonce has been taken. *)
val internal_nonce_already_recorded: context -> int -> bool

View File

@ -593,7 +593,8 @@ let rec interp
Transaction Transaction
{ amount ; destination ; { amount ; destination ;
parameters = Some (Script.lazy_expr (Micheline.strip_locations p)) } in parameters = Some (Script.lazy_expr (Micheline.strip_locations p)) } in
logged_return (Item ({ source = self ; operation }, rest), ctxt) Lwt.return (fresh_internal_nonce ctxt) >>=? fun (ctxt, nonce) ->
logged_return (Item ({ source = self ; operation ; nonce }, rest), ctxt)
| Create_account, | Create_account,
Item (manager, Item (delegate, Item (delegatable, Item (credit, rest)))) -> Item (manager, Item (delegate, Item (delegatable, Item (credit, rest)))) ->
Lwt.return (Gas.consume ctxt Interp_costs.create_account) >>=? fun ctxt -> Lwt.return (Gas.consume ctxt Interp_costs.create_account) >>=? fun ctxt ->
@ -602,7 +603,8 @@ let rec interp
Origination Origination
{ credit ; manager ; delegate ; preorigination = Some contract ; { credit ; manager ; delegate ; preorigination = Some contract ;
delegatable ; script = None ; spendable = true } in delegatable ; script = None ; spendable = true } in
logged_return (Item ({ source = self ; operation }, Lwt.return (fresh_internal_nonce ctxt) >>=? fun (ctxt, nonce) ->
logged_return (Item ({ source = self ; operation ; nonce },
Item (contract, rest)), ctxt) Item (contract, rest)), ctxt)
| Implicit_account, Item (key, rest) -> | Implicit_account, Item (key, rest) ->
Lwt.return (Gas.consume ctxt Interp_costs.implicit_account) >>=? fun ctxt -> Lwt.return (Gas.consume ctxt Interp_costs.implicit_account) >>=? fun ctxt ->
@ -631,14 +633,16 @@ let rec interp
delegatable ; spendable ; delegatable ; spendable ;
script = Some { code = Script.lazy_expr code ; script = Some { code = Script.lazy_expr code ;
storage = Script.lazy_expr storage } } in storage = Script.lazy_expr storage } } in
Lwt.return (fresh_internal_nonce ctxt) >>=? fun (ctxt, nonce) ->
logged_return logged_return
(Item ({ source = self ; operation }, (Item ({ source = self ; operation ; nonce },
Item (contract, rest)), ctxt) Item (contract, rest)), ctxt)
| Set_delegate, | Set_delegate,
Item (delegate, rest) -> Item (delegate, rest) ->
Lwt.return (Gas.consume ctxt Interp_costs.create_account) >>=? fun ctxt -> Lwt.return (Gas.consume ctxt Interp_costs.create_account) >>=? fun ctxt ->
let operation = Delegation delegate in let operation = Delegation delegate in
logged_return (Item ({ source = self ; operation }, rest), ctxt) Lwt.return (fresh_internal_nonce ctxt) >>=? fun (ctxt, nonce) ->
logged_return (Item ({ source = self ; operation ; nonce }, rest), ctxt)
| Balance, rest -> | Balance, rest ->
Lwt.return (Gas.consume ctxt Interp_costs.balance) >>=? fun ctxt -> Lwt.return (Gas.consume ctxt Interp_costs.balance) >>=? fun ctxt ->
Contract.get_balance ctxt self >>=? fun balance -> Contract.get_balance ctxt self >>=? fun balance ->