Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add tx sign/send and Stellar trait #1406

Closed
wants to merge 86 commits into from

Conversation

willemneal
Copy link
Member

@willemneal willemneal commented Jun 25, 2024

What

Currently relies on #1283

Adds:

  • tx sign - sign just a transaction (envelope), appending the signature
    • Does not have --source-acount and requires --sign-with
    • In a future PR tx auth-sign/auth will be added to handle signing just authorization entries.
  • tx send - submit a signed transaction envelope to the network
  • Stellar trait which handles the signing of transactions and auth-entries
  • Updates signing across all commands.
  • For invoke command (currently only command that handles signing auth entries), but these will used with tx auth:
    • --sign-with-key - takes same input as --source-account
    • --sign-with-lab - currently does nothing
    • --yes - if --sign-with-key user verification is required, unless --yes is provided

Why

Currently signing happens automatically in the background, which is unsure and doesn't inform users when signing has taken place.

Known limitations

Doesn't add ledger support. Still need to figure out dep issues. This is to be done in follow up PR.

@willemneal willemneal force-pushed the feat/sign_and_send branch from d8a18e7 to b8c6157 Compare June 25, 2024 20:03
@willemneal willemneal force-pushed the feat/sign_and_send branch from abf291a to b711e8a Compare June 26, 2024 17:18
@willemneal willemneal changed the title feat: add ledger support and tx sign/send feat: add tx sign/send and Stellar trait Jun 26, 2024
@willemneal willemneal marked this pull request as ready for review June 27, 2024 12:40
Before we treated the source key special from the collection of keys. Now each key can sign an auth entry in isolation
Auth entries need an expiration ledger and so we need to get the current one and add 60 to then set it before signing.
@willemneal
Copy link
Member Author

@Ifropc @leighmcculloch

I wanted to present some thoughts for how this all could be refactored. I have tested it locally.

It's a rather big change, but I propose breaking everything into smaller traits and then make use of blank implementations. I can make a follow up issue, but wanted to drop it here for thoughts and to see if it's good enough for merging this PR as is. I'm also not married to this idea, but think it is more inline with the smaller traits route. We could also just not worry with blanket impls and just implement them for each type since there weren't be a ton of signers, just thought it could help make it easier to write them.

/// A trait for signing Stellar transaction Envelopes
#[async_trait::async_trait]
pub trait TxEnv {
    /// Sign a Stellar  with the given source account
    /// This is a default implementation that signs the transaction hash and returns a decorated signature
    ///
    /// Todo: support signing the transaction directly.
    /// # Errors
    /// Returns an error if the source account is not found
    async fn sign_txn_env(
        &self,
        txn_env: TransactionEnvelope,
        network: &Network,
    ) -> Result<TransactionEnvelope, Error>;
    // let hash = transaction_hash(txn, network_passphrase)?;
    // self.sign_txn_hash(hash).await
}

/// A trait for signing Stellar transactions
#[async_trait::async_trait]
pub trait Tx {
    /// Sign a Stellar transaction with the given source account
    /// This is a default implementation that signs the transaction hash and returns a decorated signature
    ///
    /// Todo: support signing the transaction directly.
    /// # Errors
    /// Returns an error if the source account is not found
    async fn sign_txn(
        &self,
        txn: &Transaction,
        network: &Network,
    ) -> Result<DecoratedSignature, Error>;
}

/// A trait for signing Stellar transaction Hashes
#[async_trait::async_trait]
pub trait TxHash {
    /// Sign a transaction hash with the given source account
    /// # Errors
    /// Returns an error if the source account is not found
    async fn sign_txn_hash(&self, txn: [u8; 32]) -> Result<DecoratedSignature, Error>;
}

#[async_trait::async_trait]
impl<T: Blob + HasPublicKey + std::marker::Sync> TxHash for T {
    async fn sign_txn_hash(&self, txn: [u8; 32]) -> Result<DecoratedSignature, Error> {
        let source_account = self.get_public_key().await?;
        eprintln!(
            "{} about to sign hash: {}",
            source_account.to_string(),
            hex::encode(txn)
        );
        let tx_signature = self.sign_blob(&txn).await?;
        Ok(DecoratedSignature {
            // TODO: remove this unwrap. It's safe because we know the length of the array
            hint: SignatureHint(source_account.0[28..].try_into().unwrap()),
            signature: Signature(tx_signature.try_into()?),
        })
    }
}

#[async_trait::async_trait]
impl<T: TxHash + std::marker::Sync> Tx for T {
    async fn sign_txn(
        &self,
        txn: &Transaction,
        Network {
            network_passphrase, ..
        }: &Network,
    ) -> Result<DecoratedSignature, Error> {
        self.sign_txn_hash(transaction_hash(txn, network_passphrase)?)
            .await
    }
}

/// A trait for signing abritrary byte arrays
#[async_trait::async_trait]

pub trait Blob {
    /// Sign an abritatry byte array
    async fn sign_blob(&self, blob: &[u8]) -> Result<Vec<u8>, Error>;
}

#[async_trait::async_trait]
pub trait HasPublicKey {
    /// Get the public key of the signer
    async fn get_public_key(&self) -> Result<stellar_strkey::ed25519::PublicKey, Error>;
}

#[async_trait::async_trait]
impl<T: Tx + std::marker::Sync> TxEnv for T {
    async fn sign_txn_env(
        &self,
        txn_env: TransactionEnvelope,
        network: &Network,
    ) -> Result<TransactionEnvelope, Error> {
        match txn_env {
            TransactionEnvelope::Tx(TransactionV1Envelope { tx, signatures }) => {
                let decorated_signature = self.sign_txn(&tx, network).await?;
                let mut sigs = signatures.to_vec();
                sigs.push(decorated_signature);
                Ok(TransactionEnvelope::Tx(TransactionV1Envelope {
                    tx,
                    signatures: sigs.try_into()?,
                }))
            }
            _ => Err(Error::UnsupportedTransactionEnvelopeType),
        }
    }
}

pub(crate) fn hash(network_passphrase: &str) -> xdr::Hash {
    xdr::Hash(Sha256::digest(network_passphrase.as_bytes()).into())
}

pub struct LocalKey {
    key: ed25519_dalek::SigningKey,
    prompt: bool,
}

impl LocalKey {
    pub fn new(key: ed25519_dalek::SigningKey, prompt: bool) -> Self {
        Self { key, prompt }
    }
}

#[async_trait::async_trait]
impl Blob for LocalKey {
    async fn sign_blob(&self, data: &[u8]) -> Result<Vec<u8>, Error> {
        if self.prompt {
            eprintln!("Press 'y' or 'Y' for yes, any other key for no:");
            match read_key() {
                'y' | 'Y' => {
                    eprintln!("Signing now...");
                }
                _ => return Err(Error::UserCancelledSigning),
            };
        }
        let sig = self.key.sign(data);
        Ok(sig.to_bytes().to_vec())
    }
}
#[async_trait::async_trait]
impl HasPublicKey for LocalKey {
    async fn get_public_key(&self) -> Result<stellar_strkey::ed25519::PublicKey, Error> {
        Ok(stellar_strkey::ed25519::PublicKey(
            self.key.verifying_key().to_bytes(),
        ))
    }
}

@Ifropc
Copy link
Contributor

Ifropc commented Aug 16, 2024

Hey @willemneal thanks for detailed suggestion, I like the approach of breaking it down into smaller pieces!

@leighmcculloch leighmcculloch self-requested a review August 20, 2024 00:32
@leighmcculloch leighmcculloch requested a review from fnando August 27, 2024 03:44
@leighmcculloch
Copy link
Member

leighmcculloch commented Aug 27, 2024

Thanks @Ifropc for reviewing it. @fnando and I are also reviewing.

cmd/soroban-cli/src/commands/contract/invoke.rs Outdated Show resolved Hide resolved
@@ -211,6 +211,14 @@ impl Args {
KeyType::Identity.read_with_global(name, &self.local_config()?)
}

pub fn account(&self, account_str: &str) -> Result<Secret, Error> {
Copy link
Contributor

@Ifropc Ifropc Aug 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have SOROBAN_SECRET_KEY variable that we are using in stellar keys add to avoid user passing key directly. Should we support the same functionality in --sign-with-key for consistency?

# import key
SOROBAN_SECRET_KEY=SKEY stellar keys add alice --secret-key
# sign with key current
stellar contract asset deploy --asset <Asset> --source-account GSOURCE --sign-with-key SKEY 

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have STELLAR_SIGN_WITH_SECRET is this good enough? I'm confused how we would reuse STELLAR_SECRET_KEY and have it know to use sign-with-key.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the point of that env variable is to not provide the secret key directly to the command, at least my understand of it is that. I assume if we will use SOROBAN_SECRET_KEY in --sign-with-key it should be invoked like this

# Can also use sign-with-key, but it accepts a mandatory parameter which would need to be changed
SOROBAN_SECRET_KEY=SKEY stellar contract asset deploy --asset <Asset> --source-account GSOURCE --sign-with-secret 

I'm not adamant on this, and we can change it later on

cmd/soroban-cli/src/config/mod.rs Show resolved Hide resolved
@Ifropc Ifropc self-requested a review August 27, 2024 04:01
@elizabethengelman elizabethengelman mentioned this pull request Aug 29, 2024
1 task
@willemneal
Copy link
Member Author

closed in favor of: #1591

@willemneal willemneal closed this Sep 10, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: Done
Development

Successfully merging this pull request may close these issues.

Add a tx sign command
4 participants