mpp-rb 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/mpp/methods/tempo/attribution.rb +13 -0
- data/lib/mpp/methods/tempo/fee_payer_envelope.rb +21 -6
- data/lib/mpp/methods/tempo/intents.rb +49 -21
- data/lib/mpp/methods/tempo/transaction.rb +64 -7
- data/lib/mpp/version.rb +1 -1
- metadata +3 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7accbb98302b70cc76e32afc99bf2b5672c445194f79e9fd1cdfb0e5d7f058dd
|
|
4
|
+
data.tar.gz: 3538fdc0cd0851816ac29f294de441e18fefb45f0bd00b44cad6295ca6a1e1f1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b6cd8af4a9376b0db632aab578f1d9fb0e9e6d3327c926e40f35badb16750d06a15774fe7444debde1a402a7d3b51133d25bfd43d577de726fe646c35bc4f522
|
|
7
|
+
data.tar.gz: 2997bf04f49594dce0246c917075d5aba8a9f974e6ed4ff6cda586e8ae9361db110cf5197ee977fa8d795b5f0affc701f34170f4c75654f5485853608fb6776c
|
|
@@ -80,6 +80,19 @@ module Mpp
|
|
|
80
80
|
memo_server == fingerprint(server_id)
|
|
81
81
|
end
|
|
82
82
|
|
|
83
|
+
# Verify challenge-bound nonce in memo.
|
|
84
|
+
def verify_challenge_binding(memo, challenge_id)
|
|
85
|
+
return false unless challenge_id
|
|
86
|
+
return false unless mpp_memo?(memo)
|
|
87
|
+
|
|
88
|
+
begin
|
|
89
|
+
memo_nonce = [memo[52, 14]].pack("H*")
|
|
90
|
+
rescue ArgumentError
|
|
91
|
+
return false
|
|
92
|
+
end
|
|
93
|
+
memo_nonce == keccak256(challenge_id.encode(Encoding::UTF_8))[0, 7]
|
|
94
|
+
end
|
|
95
|
+
|
|
83
96
|
# Decoded memo structure.
|
|
84
97
|
DecodedMemo = Data.define(:version, :server_fingerprint, :client_fingerprint, :nonce)
|
|
85
98
|
|
|
@@ -17,8 +17,8 @@ module Mpp
|
|
|
17
17
|
Kernel.require "rlp"
|
|
18
18
|
|
|
19
19
|
sender_sig = signed_tx.sender_signature
|
|
20
|
-
sig_bytes = sender_sig.respond_to?(:to_bytes) ? sender_sig.to_bytes : sender_sig.to_s.b
|
|
21
|
-
sender_addr = signed_tx.sender_address
|
|
20
|
+
sig_bytes = normalize_signature(sender_sig.respond_to?(:to_bytes) ? sender_sig.to_bytes : sender_sig.to_s.b)
|
|
21
|
+
sender_addr = pack_hex(signed_tx.sender_address)
|
|
22
22
|
|
|
23
23
|
fields = [
|
|
24
24
|
signed_tx.chain_id,
|
|
@@ -26,12 +26,12 @@ module Mpp
|
|
|
26
26
|
signed_tx.max_fee_per_gas,
|
|
27
27
|
signed_tx.gas_limit,
|
|
28
28
|
signed_tx.calls.map(&:as_rlp_list),
|
|
29
|
-
signed_tx.access_list.map(
|
|
29
|
+
signed_tx.access_list.map { |entry| entry.respond_to?(:as_rlp_list) ? entry.as_rlp_list : entry },
|
|
30
30
|
signed_tx.nonce_key,
|
|
31
31
|
signed_tx.nonce,
|
|
32
32
|
encode_optional_uint(signed_tx.valid_before),
|
|
33
33
|
encode_optional_uint(signed_tx.valid_after),
|
|
34
|
-
signed_tx.fee_token ? signed_tx.fee_token
|
|
34
|
+
signed_tx.fee_token ? pack_hex(signed_tx.fee_token) : "".b,
|
|
35
35
|
sender_addr,
|
|
36
36
|
signed_tx.tempo_authorization_list.to_a
|
|
37
37
|
]
|
|
@@ -39,7 +39,7 @@ module Mpp
|
|
|
39
39
|
fields << RLP.decode(signed_tx.key_authorization) if signed_tx.key_authorization
|
|
40
40
|
fields << sig_bytes
|
|
41
41
|
|
|
42
|
-
[TYPE_ID].pack("C") + RLP.
|
|
42
|
+
[TYPE_ID].pack("C") + RLP.encode(fields)
|
|
43
43
|
end
|
|
44
44
|
|
|
45
45
|
# Decode a 0x78 fee payer envelope.
|
|
@@ -58,7 +58,7 @@ module Mpp
|
|
|
58
58
|
|
|
59
59
|
# 15 fields = key_authorization present (index 13), signature at 14
|
|
60
60
|
# 14 fields = no key_authorization, signature at 13
|
|
61
|
-
key_authorization = (RLP.
|
|
61
|
+
key_authorization = (RLP.encode(decoded[13]) if decoded.length == 15)
|
|
62
62
|
|
|
63
63
|
[decoded, sender_address.to_s.b, sender_signature.to_s.b, key_authorization]
|
|
64
64
|
end
|
|
@@ -68,6 +68,21 @@ module Mpp
|
|
|
68
68
|
|
|
69
69
|
value.is_a?(Integer) ? value : value.to_i
|
|
70
70
|
end
|
|
71
|
+
|
|
72
|
+
def pack_hex(value)
|
|
73
|
+
[value.to_s.delete_prefix("0x")].pack("H*")
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def normalize_signature(signature)
|
|
77
|
+
bytes = signature.b
|
|
78
|
+
Kernel.raise ArgumentError, "signature must be 65 bytes, got #{bytes.bytesize}" unless bytes.bytesize == 65
|
|
79
|
+
|
|
80
|
+
v = bytes.getbyte(64)
|
|
81
|
+
parity = (v >= 27) ? v - 27 : v
|
|
82
|
+
Kernel.raise ArgumentError, "signature parity must be 0 or 1, got #{v}" unless [0, 1].include?(parity)
|
|
83
|
+
|
|
84
|
+
bytes[0, 64] + [parity].pack("C")
|
|
85
|
+
end
|
|
71
86
|
end
|
|
72
87
|
end
|
|
73
88
|
end
|
|
@@ -51,12 +51,12 @@ module Mpp
|
|
|
51
51
|
case payload_data["type"]
|
|
52
52
|
when "hash"
|
|
53
53
|
payload = Schemas::HashCredentialPayload.new(type: "hash", hash: payload_data["hash"])
|
|
54
|
-
verify_hash(payload, req)
|
|
54
|
+
verify_hash(payload, req, credential: credential)
|
|
55
55
|
when "transaction"
|
|
56
56
|
payload = Schemas::TransactionCredentialPayload.new(
|
|
57
57
|
type: "transaction", signature: payload_data["signature"]
|
|
58
58
|
)
|
|
59
|
-
verify_transaction(payload, req)
|
|
59
|
+
verify_transaction(payload, req, credential: credential)
|
|
60
60
|
when "proof"
|
|
61
61
|
payload = Schemas::ProofCredentialPayload.new(
|
|
62
62
|
type: "proof", signature: payload_data["signature"]
|
|
@@ -75,7 +75,7 @@ module Mpp
|
|
|
75
75
|
@rpc_url
|
|
76
76
|
end
|
|
77
77
|
|
|
78
|
-
def verify_hash(payload, request)
|
|
78
|
+
def verify_hash(payload, request, credential:)
|
|
79
79
|
if @store
|
|
80
80
|
store_key = "mpp:charge:#{payload.hash.downcase}"
|
|
81
81
|
raise Mpp::VerificationError, "Transaction hash already used" unless @store.put_if_absent(store_key,
|
|
@@ -87,17 +87,17 @@ module Mpp
|
|
|
87
87
|
|
|
88
88
|
raise Mpp::VerificationError, "Transaction not found" unless result
|
|
89
89
|
raise Mpp::VerificationError, "Transaction reverted" unless result["status"] == "0x1"
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
)
|
|
90
|
+
matched_logs = match_transfer_logs(result, request, expected_sender: result["from"])
|
|
91
|
+
unless matched_logs.any?
|
|
93
92
|
raise Mpp::VerificationError,
|
|
94
93
|
"Transaction must contain a Transfer log matching request parameters"
|
|
95
94
|
end
|
|
95
|
+
assert_challenge_bound_memo(matched_logs, credential.challenge) unless request.method_details.memo
|
|
96
96
|
|
|
97
97
|
Mpp::Receipt.success(payload.hash)
|
|
98
98
|
end
|
|
99
99
|
|
|
100
|
-
def verify_transaction(payload, request)
|
|
100
|
+
def verify_transaction(payload, request, credential:)
|
|
101
101
|
validate_transaction_payload(payload.signature, request)
|
|
102
102
|
|
|
103
103
|
raw_tx = payload.signature
|
|
@@ -128,18 +128,23 @@ module Mpp
|
|
|
128
128
|
|
|
129
129
|
raise Mpp::VerificationError, "Transaction receipt not found after retries" unless receipt_data
|
|
130
130
|
raise Mpp::VerificationError, "Transaction reverted" unless receipt_data["status"] == "0x1"
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
)
|
|
131
|
+
matched_logs = match_transfer_logs(receipt_data, request, expected_sender: receipt_data["from"])
|
|
132
|
+
unless matched_logs.any?
|
|
134
133
|
raise Mpp::VerificationError,
|
|
135
134
|
"Transaction must contain a Transfer log matching request parameters"
|
|
136
135
|
end
|
|
136
|
+
assert_challenge_bound_memo(matched_logs, credential.challenge) unless request.method_details.memo
|
|
137
137
|
|
|
138
138
|
Mpp::Receipt.success(tx_hash)
|
|
139
139
|
end
|
|
140
140
|
|
|
141
141
|
def verify_transfer_logs(receipt, request, expected_sender: nil)
|
|
142
|
+
match_transfer_logs(receipt, request, expected_sender: expected_sender).any?
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def match_transfer_logs(receipt, request, expected_sender: nil)
|
|
142
146
|
expected_memo = request.method_details.memo
|
|
147
|
+
matched_logs = []
|
|
143
148
|
|
|
144
149
|
(receipt["logs"] || []).each do |log|
|
|
145
150
|
next unless log["address"]&.downcase == request.currency.downcase
|
|
@@ -164,19 +169,42 @@ module Mpp
|
|
|
164
169
|
memo = topics[3]
|
|
165
170
|
memo_clean = expected_memo.downcase
|
|
166
171
|
memo_clean = "0x#{memo_clean}" unless memo_clean.start_with?("0x")
|
|
167
|
-
|
|
172
|
+
if amount == Integer(request.amount) && memo.downcase == memo_clean
|
|
173
|
+
matched_logs << {kind: :memo, memo: memo}
|
|
174
|
+
end
|
|
168
175
|
else
|
|
169
|
-
|
|
176
|
+
case topics[0]
|
|
177
|
+
when TRANSFER_WITH_MEMO_TOPIC
|
|
178
|
+
next if topics.length < 4
|
|
179
|
+
|
|
180
|
+
data = log.fetch("data", "0x")
|
|
181
|
+
next if data.length < 66
|
|
182
|
+
|
|
183
|
+
amount = data[2, 64].to_i(16)
|
|
184
|
+
matched_logs << {kind: :memo, memo: topics[3]} if amount == Integer(request.amount)
|
|
185
|
+
when TRANSFER_TOPIC
|
|
186
|
+
data = log.fetch("data", "0x")
|
|
187
|
+
next if data.length < 66
|
|
188
|
+
|
|
189
|
+
amount = data.delete_prefix("0x").to_i(16)
|
|
190
|
+
matched_logs << {kind: :transfer} if amount == Integer(request.amount)
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
170
194
|
|
|
171
|
-
|
|
172
|
-
|
|
195
|
+
matched_logs.sort_by { |log| (log[:kind] == :memo) ? 0 : 1 }
|
|
196
|
+
end
|
|
173
197
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
198
|
+
def assert_challenge_bound_memo(matched_logs, challenge)
|
|
199
|
+
bound = matched_logs.any? do |log|
|
|
200
|
+
log[:kind] == :memo &&
|
|
201
|
+
Attribution.verify_server(log[:memo], challenge.realm) &&
|
|
202
|
+
Attribution.verify_challenge_binding(log[:memo], challenge.id)
|
|
177
203
|
end
|
|
178
204
|
|
|
179
|
-
|
|
205
|
+
return if bound
|
|
206
|
+
|
|
207
|
+
raise Mpp::VerificationError, "Payment verification failed: memo is not bound to this challenge"
|
|
180
208
|
end
|
|
181
209
|
|
|
182
210
|
def validate_transaction_payload(signature, request)
|
|
@@ -335,7 +363,7 @@ module Mpp
|
|
|
335
363
|
max_fee_per_gas: int.call(decoded[2]),
|
|
336
364
|
gas_limit: int.call(decoded[3]),
|
|
337
365
|
calls: calls,
|
|
338
|
-
access_list: Transaction::EMPTY_LIST,
|
|
366
|
+
access_list: decoded[5] || Transaction::EMPTY_LIST,
|
|
339
367
|
nonce_key: int.call(decoded[6]),
|
|
340
368
|
nonce: int.call(decoded[7]),
|
|
341
369
|
valid_before: int.call(decoded[8]),
|
|
@@ -350,7 +378,7 @@ module Mpp
|
|
|
350
378
|
|
|
351
379
|
# Verify sender signature
|
|
352
380
|
sender_hash = tx_for_recovery.signature_hash
|
|
353
|
-
recovered = Eth::
|
|
381
|
+
recovered = Eth::Signature.recover(sender_hash, "0x#{sender_sig.unpack1("H*")}")
|
|
354
382
|
recovered_addr = Eth::Util.public_key_to_address(recovered).to_s
|
|
355
383
|
envelope_addr = "0x#{sender_addr_bytes.unpack1("H*")}"
|
|
356
384
|
|
|
@@ -364,7 +392,7 @@ module Mpp
|
|
|
364
392
|
|
|
365
393
|
tx_to_sign = tx_for_recovery.with(fee_token: resolved_fee_token)
|
|
366
394
|
|
|
367
|
-
# Fee payer signs
|
|
395
|
+
# Fee payer signs the 0x78 payload, which identifies the recovered sender.
|
|
368
396
|
fee_payer_hash = tx_to_sign.fee_payer_signature_hash
|
|
369
397
|
fee_payer_sig = fee_payer.sign_hash(fee_payer_hash)
|
|
370
398
|
|
|
@@ -45,29 +45,50 @@ module Mpp
|
|
|
45
45
|
require_eth!
|
|
46
46
|
require_rlp!
|
|
47
47
|
|
|
48
|
-
Eth::Util.keccak256([TYPE_ID].pack("C") + RLP.encode(
|
|
48
|
+
Eth::Util.keccak256([TYPE_ID].pack("C") + RLP.encode(signing_rlp_fields))
|
|
49
49
|
end
|
|
50
50
|
|
|
51
|
-
# Hash for fee payer to sign
|
|
51
|
+
# Hash for fee payer to sign.
|
|
52
52
|
def fee_payer_signature_hash
|
|
53
53
|
require_eth!
|
|
54
54
|
require_rlp!
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
fields.insert(11, sender_signature)
|
|
58
|
-
Eth::Util.keccak256([TYPE_ID].pack("C") + RLP.encode(fields))
|
|
56
|
+
Eth::Util.keccak256([FeePayer::TYPE_ID].pack("C") + RLP.encode(fee_payer_signing_rlp_fields))
|
|
59
57
|
end
|
|
60
58
|
|
|
61
59
|
private
|
|
62
60
|
|
|
63
61
|
def rlp_fields
|
|
62
|
+
fields = signing_rlp_fields
|
|
63
|
+
fields << signature_envelope(sender_signature) if sender_signature
|
|
64
|
+
fields
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def signing_rlp_fields
|
|
64
68
|
fields = unsigned_rlp_fields
|
|
65
|
-
fields
|
|
66
|
-
fields.insert(12, fee_payer_signature || EMPTY_SIGNATURE)
|
|
69
|
+
fields << key_authorization if key_authorization
|
|
67
70
|
fields
|
|
68
71
|
end
|
|
69
72
|
|
|
70
73
|
def unsigned_rlp_fields
|
|
74
|
+
[
|
|
75
|
+
chain_id,
|
|
76
|
+
max_priority_fee_per_gas,
|
|
77
|
+
max_fee_per_gas,
|
|
78
|
+
gas_limit,
|
|
79
|
+
calls.map(&:as_rlp_list),
|
|
80
|
+
access_list || EMPTY_LIST,
|
|
81
|
+
nonce_key,
|
|
82
|
+
nonce,
|
|
83
|
+
encode_optional_uint(valid_before),
|
|
84
|
+
encode_optional_uint(valid_after),
|
|
85
|
+
fee_token ? pack_hex(fee_token) : "".b,
|
|
86
|
+
fee_payer_field,
|
|
87
|
+
tempo_authorization_list || EMPTY_LIST
|
|
88
|
+
]
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def fee_payer_signing_rlp_fields
|
|
71
92
|
fields = [
|
|
72
93
|
chain_id,
|
|
73
94
|
max_priority_fee_per_gas,
|
|
@@ -80,12 +101,48 @@ module Mpp
|
|
|
80
101
|
encode_optional_uint(valid_before),
|
|
81
102
|
encode_optional_uint(valid_after),
|
|
82
103
|
fee_token ? pack_hex(fee_token) : "".b,
|
|
104
|
+
pack_hex(sender_address),
|
|
83
105
|
tempo_authorization_list || EMPTY_LIST
|
|
84
106
|
]
|
|
85
107
|
fields << key_authorization if key_authorization
|
|
86
108
|
fields
|
|
87
109
|
end
|
|
88
110
|
|
|
111
|
+
def fee_payer_field
|
|
112
|
+
return signature_tuple(fee_payer_signature) if fee_payer_signature && fee_payer_signature != EMPTY_SIGNATURE
|
|
113
|
+
return EMPTY_SIGNATURE if fee_token.nil?
|
|
114
|
+
|
|
115
|
+
"".b
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def signature_tuple(signature)
|
|
119
|
+
normalized = normalized_signature(signature)
|
|
120
|
+
[
|
|
121
|
+
normalized.getbyte(64).zero? ? "".b : normalized[64],
|
|
122
|
+
trim_leading_zeroes(normalized[0, 32]),
|
|
123
|
+
trim_leading_zeroes(normalized[32, 32])
|
|
124
|
+
]
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def signature_envelope(signature)
|
|
128
|
+
normalized_signature(signature)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def normalized_signature(signature)
|
|
132
|
+
bytes = signature.b
|
|
133
|
+
raise ArgumentError, "signature must be 65 bytes, got #{bytes.bytesize}" unless bytes.bytesize == 65
|
|
134
|
+
|
|
135
|
+
v = bytes.getbyte(64)
|
|
136
|
+
parity = (v >= 27) ? v - 27 : v
|
|
137
|
+
raise ArgumentError, "signature parity must be 0 or 1, got #{v}" unless [0, 1].include?(parity)
|
|
138
|
+
|
|
139
|
+
bytes[0, 64] + [parity].pack("C")
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def trim_leading_zeroes(value)
|
|
143
|
+
value.sub(/\A\x00+/n, "")
|
|
144
|
+
end
|
|
145
|
+
|
|
89
146
|
def pack_hex(value)
|
|
90
147
|
[value.delete_prefix("0x")].pack("H*")
|
|
91
148
|
end
|
data/lib/mpp/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: mpp-rb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Stripe
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date:
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: base64
|
|
@@ -26,7 +25,6 @@ dependencies:
|
|
|
26
25
|
version: '0.3'
|
|
27
26
|
description: Ruby SDK for the Machine Payments Protocol (MPP) — an HTTP 402 Payment
|
|
28
27
|
Authentication scheme.
|
|
29
|
-
email:
|
|
30
28
|
executables: []
|
|
31
29
|
extensions: []
|
|
32
30
|
extra_rdoc_files: []
|
|
@@ -89,7 +87,6 @@ licenses:
|
|
|
89
87
|
metadata:
|
|
90
88
|
rubygems_mfa_required: 'true'
|
|
91
89
|
source_code_uri: https://github.com/stripe/mpp-rb
|
|
92
|
-
post_install_message:
|
|
93
90
|
rdoc_options: []
|
|
94
91
|
require_paths:
|
|
95
92
|
- lib
|
|
@@ -104,8 +101,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
104
101
|
- !ruby/object:Gem::Version
|
|
105
102
|
version: '0'
|
|
106
103
|
requirements: []
|
|
107
|
-
rubygems_version: 3.
|
|
108
|
-
signing_key:
|
|
104
|
+
rubygems_version: 3.6.9
|
|
109
105
|
specification_version: 4
|
|
110
106
|
summary: HTTP 402 Payment Authentication for Ruby
|
|
111
107
|
test_files: []
|