Conversation
There was a problem hiding this comment.
Pull request overview
This pull request adds XRPL (XRP Ledger) signing support to the FKMS (Falcon Key Management Service) alongside existing EVM functionality. The changes introduce TSS (Threshold Signature Scheme) signature verification, XRPL-specific transaction encoding, and extend the gRPC API with new RPC methods for XRPL signing. The implementation includes codec modules for decoding TSS messages (with tick and fixed-point price encoding), EVM transactions, and XRPL transaction construction.
Changes:
- Added TSS signature verification system with challenge-based verification for secure transaction signing
- Implemented XRPL signing support with DER signature format and compressed public key handling
- Extended proto interface with SignXrpl RPC method and supporting message types for XRPL transactions
Reviewed changes
Copilot reviewed 27 out of 28 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
src/verifier/tss/signature.rs |
New TSS signature verifier using Schnorr-like verification with challenge computation |
src/verifier/tss/message.rs |
TSS message verification comparing prices from transaction and TSS data |
src/codec/tss.rs |
TSS message decoder supporting fixed-point and tick-based price encoding |
src/codec/evm.rs |
EVM transaction decoder extracting TSS data from legacy and EIP-1559 transactions |
src/codec/xrpl.rs |
XRPL transaction encoder creating OracleSet transactions with price data |
src/signer.rs |
Added XrplSigner trait and XRPL address derivation using RIPEMD-160 |
src/signer/local.rs |
Extended LocalSigner to support both ECDSA and DER signatures with compressed keys |
src/signer/aws.rs |
Updated AWS signer interface to support is_compressed parameter |
src/signer/signature/ecdsa.rs |
Added DerSignature type for XRPL signing |
src/server/service.rs |
New unified service implementation with sign_evm and sign_xrpl endpoints |
src/server/builder.rs |
Extended builder to support XRPL signers and TSS verifier |
src/server.rs |
Updated server struct to hold both EVM and XRPL signers |
src/server/pre_sign.rs |
Changed PreSignHook interface to accept prices instead of raw message |
src/config/signer/local.rs |
Added ChainType enum to distinguish EVM and XRPL configurations |
src/commands/utils.rs |
Separated signer loading logic for EVM (coin_type 60) and XRPL (coin_type 144) |
src/commands/start.rs |
Added TSS public key loading from environment variable |
src/commands/key.rs |
Updated key display to show both EVM and XRPL addresses |
proto/fkms/v1/signer.proto |
Added SignXrpl RPC method and supporting message types |
Cargo.toml |
Added dependencies for XRPL support including xrpl-rust and xrpl_binary_codec |
.github/workflows/ci.yml |
Updated Rust toolchain version |
README.md |
Updated documentation to reflect XRPL support |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 25 out of 26 changed files in this pull request and generated 9 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 28 out of 29 changed files in this pull request and generated 10 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 28 out of 29 changed files in this pull request and generated 9 comments.
Comments suppressed due to low confidence (1)
src/signer/aws.rs:72
AwsSigner::sign_ecdsa_internalusesSigningAlgorithmSpec::EcdsaSha256without settingmessage_typetoDIGEST, so AWS KMS will hash the provided bytes with SHA-256 before signing. The newSigner::sign_ecdsainterface is being called with a prehash (Keccak256 digest) inserver/service.rs, so the AWS signer will produce signatures overSHA256(keccak(tx))while the local signer signskeccak(tx)directly. Align AWS signing behavior with the prehash contract (e.g., setmessage_typeappropriately and/or adjust what is passed to KMS).
let sign_output = self
.client
.sign()
.key_id(&self.key_id)
.message(Blob::new(message))
.signing_algorithm(aws_sdk_kms::types::SigningAlgorithmSpec::EcdsaSha256)
.send()
.await?;
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 28 out of 29 changed files in this pull request and generated 7 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 28 out of 29 changed files in this pull request and generated 6 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
You can also share your feedback on Copilot code review. Take the survey.
| @@ -52,35 +73,48 @@ impl AwsSigner { | |||
| .send() | |||
| .await?; | |||
|
|
|||
| let signature_blob = sign_output | |||
| .signature | |||
| .ok_or(anyhow::Error::msg("no signature found"))?; | |||
| let signature_blob = sign_output.signature.ok_or(anyhow!("no signature found"))?; | |||
|
|
|||
| let signature = ecdsa::Signature::from_der(signature_blob.as_ref())?; | |||
| match self.chain_type { | |||
| ChainType::Evm => { | |||
| let signature = ecdsa::Signature::from_der(signature_blob.as_ref())?; | |||
| let digest = Keccak256::new_with_prefix(message); | |||
| let recovery_id = self.find_recovery_id(digest, &signature)?; | |||
| let recoverable_signature: EcdsaSignature = (signature, recovery_id); | |||
| Ok(recoverable_signature.into_vec()) | |||
| } | |||
| _ => Err(anyhow!("Unsupported Chain Type")), | |||
| } | |||
There was a problem hiding this comment.
AwsSigner::sign sends message directly to KMS with SigningAlgorithmSpec::EcdsaSha256 but does not set message_type = DIGEST. In the server, message is already a 32-byte Keccak256 digest, so KMS will hash it with SHA-256 again, producing an invalid signature for EVM usage. Also, recovery-id search re-hashes message via Keccak256::new_with_prefix(message); if message is already a digest, recovery should use a prehash recovery API (or use the exact digest bytes that were signed).
| pub fn public_key_to_evm_address(public_key: &[u8]) -> anyhow::Result<String> { | ||
| // Check exact length | ||
| if public_key.len() != 65 { | ||
| return Err(anyhow!( | ||
| "Invalid public key length for EVM address. Expected 65 bytes, got {}", | ||
| public_key.len() | ||
| )); | ||
| } | ||
| } | ||
|
|
||
| fn public_key_to_evm_address(public_key: &[u8]) -> String { | ||
| let mut hasher = sha3::Keccak256::new(); | ||
| hasher.update(&public_key[1..]); // Skip the first byte (0x04) | ||
| let hash = hasher.finalize().to_vec(); | ||
| format!("0x{}", hex::encode(&hash[12..])) | ||
|
|
||
| // We can now safely skip the first byte because we validated length and prefix | ||
| hasher.update(&public_key[1..]); | ||
| let hash = hasher.finalize(); |
There was a problem hiding this comment.
public_key_to_evm_address only validates the key length, but the comment says the prefix is validated and the implementation always skips public_key[0]. If a non-uncompressed SEC1 key (or wrong prefix) is passed, this will silently compute the wrong address. Consider validating public_key[0] == 0x04 (and returning an error otherwise) or explicitly accepting both compressed/uncompressed forms and normalizing first.
| async fn test_sign_der() { | ||
| let pk = hex::decode("45ea1ad910df93d1a021a42f384aad55c7c65d565ad6b2203a4ba50418922a7b") | ||
| .unwrap(); | ||
| let signer = LocalSigner::new(&pk, &ChainType::Evm).unwrap(); |
There was a problem hiding this comment.
test_sign_der constructs the signer with ChainType::Evm, but DER signing is only used for XRPL in LocalSigner::sign. This makes the test inconsistent with the production behavior and could hide regressions in XRPL-specific setup (compressed pubkey/address). Update the test to use ChainType::Xrpl for the signer instance.
| let signer = LocalSigner::new(&pk, &ChainType::Evm).unwrap(); | |
| let signer = LocalSigner::new(&pk, &ChainType::Xrpl).unwrap(); |
| @@ -139,6 +179,50 @@ | |||
| } | |||
| ``` | |||
There was a problem hiding this comment.
README's GetSignerAddressesResponse example still shows repeated string addresses, but the proto was changed to repeated Signers signers. This is misleading for API consumers; update the example to match proto/fkms/v1/signer.proto.
| // Convert characters to Hex uppercase | ||
| let mut hex_str = s.chars().fold(String::new(), |mut acc, c| { | ||
| acc.push_str(&format!("{:02X}", c as u32)); | ||
| acc | ||
| }); | ||
|
|
There was a problem hiding this comment.
str_to_hex claims to only support ASCII, but it operates on chars without enforcing is_ascii(). Non-ASCII input will produce variable-length hex (e.g., >2 bytes per char), potentially violating XRPL field-size expectations and breaking encoding. Consider validating s.is_ascii() and encoding via s.as_bytes() instead of iterating chars.
| // Convert characters to Hex uppercase | |
| let mut hex_str = s.chars().fold(String::new(), |mut acc, c| { | |
| acc.push_str(&format!("{:02X}", c as u32)); | |
| acc | |
| }); | |
| // Enforce ASCII-only input to avoid variable-length and invalid encodings | |
| if !s.is_ascii() { | |
| return Err(anyhow!( | |
| "str_to_hex only supports ASCII input, got non-ASCII string: {}", | |
| s | |
| )); | |
| } | |
| // Convert bytes to uppercase hex (two hex chars per byte) | |
| let mut hex_str = String::new(); | |
| for b in s.as_bytes() { | |
| hex_str.push_str(&format!("{:02X}", b)); | |
| } |
| [[signer_config.local_signer_configs]] | ||
| type = "private_key" | ||
| env_variable = "PRIVATE_KEY_1" | ||
| encoding = "hex" | ||
| chain_type = "evm" | ||
|
|
||
| [[signer_config.local_signer_configs]] | ||
| type = "mnemonic" | ||
| env_variable = "MNEMONIC_1" | ||
| coin_type = 60 | ||
| account = 0 | ||
| index = 0 | ||
|
|
||
| [tss] | ||
| enable_verify = true | ||
|
|
||
| [[tss.groups]] | ||
| public_key = "0232a786de4c99679f10fb9a1c94d17737739e413bef385a2f8a26f901a40fdc8b" | ||
| expired_time = 1234567890 | ||
| ``` | ||
|
|
||
| ### Supported Local Signer Types | ||
|
|
||
| | Type | Description | Required Fields | | ||
| | --------------| ---------------------------------------------- | -----------------------------------------------| | ||
| | `private_key` | Load private key from an environment variable | `env_variable`, `encoding` | | ||
| | `private_key` | Load private key from an environment variable (Currently support only EVM) | `env_variable`, `encoding` | | ||
| | `mnemonic` | Load mnemonic from an environment variable | `env_variable`, `coin_type`, `account`, `index`| | ||
|
|
There was a problem hiding this comment.
The sample config.toml is missing chain_type for the mnemonic signer config, but the code requires it. Also, the table says private_key currently supports only EVM, but LocalSigner::new supports both EVM and XRPL based on chain_type. Please update the example/table to match the actual configuration requirements and supported chains.
Add xrpl sign logic
codecdir