Module: Mpp::Methods::Tempo::Proof
- Defined in:
- lib/mpp/methods/tempo/proof.rb
Overview
EIP-712 proof credentials for zero-amount challenges.
Domain: { name: "MPP", version: "3", chainId }
Types: { Proof: [{ name: "account", type: "address" },
{ name: "challengeId", type: "string" },
{ name: "realm", type: "string" }] }
Message: { account:
The account field binds the signature to one payer wallet, so a proof signed for one account cannot be replayed against another. This matches mpp-go, mpp-rs and mppx, which all sign and verify domain version "3" and reject version "2".
Constant Summary collapse
- DOMAIN_NAME =
"MPP"- DOMAIN_VERSION =
"3"- DOMAIN_TYPE_HASH =
EIP-712 domain separator type hash
"EIP712Domain(string name,string version,uint256 chainId)"- PROOF_TYPE_HASH =
"Proof(address account,string challengeId,string realm)"
Class Method Summary collapse
-
.abi_encode(*values) ⇒ Object
ABI-encode values (packed 32-byte words).
-
.address_word(address) ⇒ Object
Encode an address as a 32-byte EIP-712 word.
-
.domain_separator(chain_id) ⇒ Object
Compute the EIP-712 domain separator.
- .keccak256(data) ⇒ Object
-
.parse_source(source_str) ⇒ Object
Parse a proof source DID.
- .recover_address(hash, sig_bytes) ⇒ Object
-
.sign(account:, chain_id:, challenge_id:, realm:) ⇒ Object
Sign a proof credential (client-side).
-
.signing_hash(chain_id:, account:, challenge_id:, realm:) ⇒ Object
Compute the full EIP-712 signing hash.
-
.source(address:, chain_id:) ⇒ Object
Construct source DID for proof credentials.
-
.struct_hash(account, challenge_id, realm) ⇒ Object
Compute the EIP-712 struct hash for Proof(account, challengeId, realm).
- .uint256(value) ⇒ Object
-
.verify(address:, chain_id:, challenge_id:, realm:, signature:) ⇒ Object
Verify a proof credential signature (server-side).
Class Method Details
.abi_encode(*values) ⇒ Object
ABI-encode values (packed 32-byte words).
125 126 127 128 129 130 131 132 133 134 |
# File 'lib/mpp/methods/tempo/proof.rb', line 125 def abi_encode(*values) values.map { |v| case v when String v.b.rjust(32, "\x00".b) when Integer uint256(v) end }.join end |
.address_word(address) ⇒ Object
Encode an address as a 32-byte EIP-712 word. Unlike a string field the address is not hashed, it is the raw 20 bytes left-padded to 32.
117 118 119 120 121 122 |
# File 'lib/mpp/methods/tempo/proof.rb', line 117 def address_word(address) hex = address.to_s.delete_prefix("0x") raise ArgumentError, "invalid address: #{address}" unless hex.match?(/\A[a-fA-F0-9]{40}\z/) [hex].pack("H*") end |
.domain_separator(chain_id) ⇒ Object
Compute the EIP-712 domain separator.
35 36 37 38 39 40 41 42 43 44 |
# File 'lib/mpp/methods/tempo/proof.rb', line 35 def domain_separator(chain_id) keccak256( abi_encode( keccak256(DOMAIN_TYPE_HASH), keccak256(DOMAIN_NAME), keccak256(DOMAIN_VERSION), uint256(chain_id) ) ) end |
.keccak256(data) ⇒ Object
29 30 31 32 |
# File 'lib/mpp/methods/tempo/proof.rb', line 29 def keccak256(data) Kernel.require "eth" Eth::Util.keccak256(data) end |
.parse_source(source_str) ⇒ Object
Parse a proof source DID. Returns { address:, chain_id: } or nil.
102 103 104 105 106 107 108 109 110 111 112 113 |
# File 'lib/mpp/methods/tempo/proof.rb', line 102 def parse_source(source_str) match = source_str.match(/\Adid:pkh:eip155:(0|[1-9]\d*):(.+)\z/) return nil unless match chain_id = Integer(match[1]) address = match[2] return nil unless address.match?(/\A0x[a-fA-F0-9]{40}\z/) {address: address, chain_id: chain_id} rescue ArgumentError nil end |
.recover_address(hash, sig_bytes) ⇒ Object
143 144 145 146 147 148 149 150 151 152 153 154 |
# File 'lib/mpp/methods/tempo/proof.rb', line 143 def recover_address(hash, sig_bytes) Kernel.require "eth" return nil unless sig_bytes.bytesize == 65 sig_hex = "0x#{sig_bytes.unpack1("H*")}" # Use raw ecrecover (not personal_recover which adds EIP-191 prefix) recovered_key = Eth::Signature.recover(hash, sig_hex) Eth::Util.public_key_to_address(recovered_key).to_s rescue => _e nil end |
.sign(account:, chain_id:, challenge_id:, realm:) ⇒ Object
Sign a proof credential (client-side). The signer is also the payer.
66 67 68 69 70 71 72 73 74 75 |
# File 'lib/mpp/methods/tempo/proof.rb', line 66 def sign(account:, chain_id:, challenge_id:, realm:) hash = signing_hash( chain_id: chain_id, account: account.address.to_s, challenge_id: challenge_id, realm: realm ) sig = account.sign_hash(hash) "0x#{sig.unpack1("H*")}" end |
.signing_hash(chain_id:, account:, challenge_id:, realm:) ⇒ Object
Compute the full EIP-712 signing hash.
59 60 61 62 63 |
# File 'lib/mpp/methods/tempo/proof.rb', line 59 def signing_hash(chain_id:, account:, challenge_id:, realm:) keccak256( "\x19\x01".b + domain_separator(chain_id) + struct_hash(account, challenge_id, realm) ) end |
.source(address:, chain_id:) ⇒ Object
Construct source DID for proof credentials.
97 98 99 |
# File 'lib/mpp/methods/tempo/proof.rb', line 97 def source(address:, chain_id:) "did:pkh:eip155:#{chain_id}:#{address}" end |
.struct_hash(account, challenge_id, realm) ⇒ Object
Compute the EIP-712 struct hash for Proof(account, challengeId, realm).
47 48 49 50 51 52 53 54 55 56 |
# File 'lib/mpp/methods/tempo/proof.rb', line 47 def struct_hash(account, challenge_id, realm) keccak256( abi_encode( keccak256(PROOF_TYPE_HASH), address_word(account), keccak256(challenge_id), keccak256(realm) ) ) end |
.uint256(value) ⇒ Object
136 137 138 139 140 141 |
# File 'lib/mpp/methods/tempo/proof.rb', line 136 def uint256(value) value = Integer(value) raise ArgumentError, "uint256 out of range" if value.negative? || value >= (1 << 256) [value.to_s(16).rjust(64, "0")].pack("H*") end |
.verify(address:, chain_id:, challenge_id:, realm:, signature:) ⇒ Object
Verify a proof credential signature (server-side).
78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 |
# File 'lib/mpp/methods/tempo/proof.rb', line 78 def verify(address:, chain_id:, challenge_id:, realm:, signature:) Kernel.require "eth" hash = signing_hash( chain_id: chain_id, account: address, challenge_id: challenge_id, realm: realm ) sig_bytes = [signature.delete_prefix("0x")].pack("H*") # Recover the signer address from the signature recovered = recover_address(hash, sig_bytes) return false unless recovered recovered.downcase == address.downcase end |