Add Counterparty signing support

- Add a new parameter "signature_target" to "sign". Supports single and
  multisign, but I haven't written tests for multisign yet.
- Skip account validation if this field is set, like multisigning.
- Unit tests demonstrating examples.
This commit is contained in:
Ed Hennis
2025-08-09 00:55:00 -04:00
parent 6778521f12
commit 6d137e44dc
10 changed files with 398 additions and 34 deletions

View File

@@ -23,6 +23,7 @@
#include <xrpl/basics/ByteUtilities.h> #include <xrpl/basics/ByteUtilities.h>
#include <xrpl/basics/base_uint.h> #include <xrpl/basics/base_uint.h>
#include <xrpl/basics/partitioned_unordered_map.h> #include <xrpl/basics/partitioned_unordered_map.h>
#include <xrpl/protocol/Units.h>
#include <cstdint> #include <cstdint>

View File

@@ -389,6 +389,8 @@ public:
setFieldV256(SField const& field, STVector256 const& v); setFieldV256(SField const& field, STVector256 const& v);
void void
setFieldArray(SField const& field, STArray const& v); setFieldArray(SField const& field, STArray const& v);
void
setFieldObject(SField const& field, STObject const& v);
template <class Tag> template <class Tag>
void void

View File

@@ -125,7 +125,11 @@ public:
getJson(JsonOptions options, bool binary) const; getJson(JsonOptions options, bool binary) const;
void void
sign(PublicKey const& publicKey, SecretKey const& secretKey); sign(
PublicKey const& publicKey,
SecretKey const& secretKey,
std::optional<std::reference_wrapper<SField const>> signatureTarget =
{});
enum class RequireFullyCanonicalSig : bool { no, yes }; enum class RequireFullyCanonicalSig : bool { no, yes };

View File

@@ -59,6 +59,7 @@ JSS(BaseAsset); // in: Oracle
JSS(BidMax); // in: AMM Bid JSS(BidMax); // in: AMM Bid
JSS(BidMin); // in: AMM Bid JSS(BidMin); // in: AMM Bid
JSS(ClearFlag); // field. JSS(ClearFlag); // field.
JSS(Counterparty); // field.
JSS(CounterpartySignature);// field. JSS(CounterpartySignature);// field.
JSS(DeliverMax); // out: alias to Amount JSS(DeliverMax); // out: alias to Amount
JSS(DeliverMin); // in: TransactionSign JSS(DeliverMin); // in: TransactionSign
@@ -567,6 +568,7 @@ JSS(settle_delay); // out: AccountChannels
JSS(severity); // in: LogLevel JSS(severity); // in: LogLevel
JSS(shares); // out: VaultInfo JSS(shares); // out: VaultInfo
JSS(signature); // out: NetworkOPs, ChannelAuthorize JSS(signature); // out: NetworkOPs, ChannelAuthorize
JSS(signature_target); // in: TransactionSign
JSS(signature_verified); // out: ChannelVerify JSS(signature_verified); // out: ChannelVerify
JSS(signing_key); // out: NetworkOPs JSS(signing_key); // out: NetworkOPs
JSS(signing_keys); // out: ValidatorList JSS(signing_keys); // out: ValidatorList

View File

@@ -831,6 +831,12 @@ STObject::setFieldArray(SField const& field, STArray const& v)
setFieldUsingAssignment(field, v); setFieldUsingAssignment(field, v);
} }
void
STObject::setFieldObject(SField const& field, STObject const& v)
{
setFieldUsingAssignment(field, v);
}
Json::Value Json::Value
STObject::getJson(JsonOptions options) const STObject::getJson(JsonOptions options) const
{ {

View File

@@ -234,13 +234,24 @@ STTx::getSeqValue() const
} }
void void
STTx::sign(PublicKey const& publicKey, SecretKey const& secretKey) STTx::sign(
PublicKey const& publicKey,
SecretKey const& secretKey,
std::optional<std::reference_wrapper<SField const>> signatureTarget)
{ {
auto const data = getSigningData(*this); auto const data = getSigningData(*this);
auto const sig = ripple::sign(publicKey, secretKey, makeSlice(data)); auto const sig = ripple::sign(publicKey, secretKey, makeSlice(data));
setFieldVL(sfTxnSignature, sig); if (signatureTarget)
{
auto& target = peekFieldObject(*signatureTarget);
target.setFieldVL(sfTxnSignature, sig);
}
else
{
setFieldVL(sfTxnSignature, sig);
}
tid_ = getHash(HashPrefix::transactionID); tid_ = getHash(HashPrefix::transactionID);
} }

View File

@@ -37,6 +37,7 @@
#include <xrpl/basics/base_uint.h> #include <xrpl/basics/base_uint.h>
#include <xrpl/beast/unit_test/suite.h> #include <xrpl/beast/unit_test/suite.h>
#include <xrpl/json/json_forwards.h> #include <xrpl/json/json_forwards.h>
#include <xrpl/json/json_reader.h>
#include <xrpl/protocol/Asset.h> #include <xrpl/protocol/Asset.h>
#include <xrpl/protocol/Feature.h> #include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h> #include <xrpl/protocol/Indexes.h>
@@ -2162,31 +2163,301 @@ class Loan_test : public beast::unit_test::suite
Env env(*this, all); Env env(*this, all);
Account const alice{"alice"}; Account const alice{"alice"};
std::string const borrowerPass = "borrower";
std::string const borrowerSeed = "ssBRAsLpH4778sLNYC4ik1JBJsBVf";
Account borrower{borrowerPass, KeyType::ed25519};
auto const lenderPass = "lender";
std::string const lenderSeed = "shPTCZGwTEhJrYT8NbcNkeaa8pzPM";
Account lender{lenderPass, KeyType::ed25519};
env.fund(XRP(1'000'000), alice, lender, borrower);
env.close();
env(noop(lender));
env(noop(lender));
env(noop(lender));
env(noop(lender));
env(noop(lender));
env.close();
{ {
auto const sk = alice.sk(); testcase("RPC AccountSet");
auto const jr = env.rpc( Json::Value txJson{Json::objectValue};
"sign", txJson[sfTransactionType] = "AccountSet";
encodeBase58Token(TokenType::FamilySeed, sk.data(), sk.size()), txJson[sfAccount] = borrower.human();
R"({ "TransactionType" : "LoanSet", )"
R"("Account" : "rHP9W1SByc8on4vfFsBdt5sun2gZTnBhkx", )" auto const signParams = [&]() {
R"("Counterparty" : "rHP9W1SByc8on4vfFsBdt5sun2gZTnBhkx", )" Json::Value signParams{Json::objectValue};
R"("LoanBrokerID" : )" signParams[jss::passphrase] = borrowerPass;
R"("EAFD2D37FE12815F00705F1B57173A16F94EE15E02D53AF5B683942B57ED53E9", )" signParams[jss::key_type] = "ed25519";
R"("PrincipalRequested" : "100000000", )" signParams[jss::tx_json] = txJson;
R"("StartDate" : "807730340", "PaymentTotal" : 10000, )" return signParams;
R"("PaymentInterval" : 1, "GracePeriod" : 300, )" }();
R"("Flags" : 65536, "Fee" : "24", "Sequence" : 1 })", auto const jSign = env.rpc("json", "sign", to_string(signParams));
"offline"); BEAST_EXPECT(
jSign.isMember(jss::result) &&
jSign[jss::result].isMember(jss::tx_json));
auto txSignResult = jSign[jss::result][jss::tx_json];
auto txSignBlob = jSign[jss::result][jss::tx_blob].asString();
txSignResult.removeMember(jss::hash);
auto const jtx = env.jt(txJson, sig(borrower));
BEAST_EXPECT(txSignResult == jtx.jv);
auto const jSubmit = env.rpc("submit", txSignBlob);
BEAST_EXPECT(
jSubmit.isMember(jss::result) &&
jSubmit[jss::result].isMember(jss::engine_result) &&
jSubmit[jss::result][jss::engine_result].asString() ==
"tesSUCCESS");
env(jtx.jv, sig(none), seq(none), fee(none), ter(tefPAST_SEQ));
}
{
testcase("RPC LoanSet - illegal signature_target");
Json::Value txJson{Json::objectValue};
txJson[sfTransactionType] = "AccountSet";
txJson[sfAccount] = borrower.human();
auto const borrowerSignParams = [&]() {
Json::Value params{Json::objectValue};
params[jss::passphrase] = borrowerPass;
params[jss::key_type] = "ed25519";
params[jss::signature_target] = "Destination";
params[jss::tx_json] = txJson;
return params;
}();
auto const jSignBorrower =
env.rpc("json", "sign", to_string(borrowerSignParams));
BEAST_EXPECT(
jSignBorrower.isMember(jss::result) &&
jSignBorrower[jss::result].isMember(jss::error) &&
jSignBorrower[jss::result][jss::error] == "invalidParams" &&
jSignBorrower[jss::result].isMember(jss::error_message) &&
jSignBorrower[jss::result][jss::error_message] ==
"Destination");
}
{
testcase("RPC LoanSet - sign and submit borrower initiated");
// 1. Borrower creates the transaction
Json::Value txJson{Json::objectValue};
txJson[sfTransactionType] = "LoanSet";
txJson[sfAccount] = borrower.human();
txJson[sfCounterparty] = lender.human();
txJson[sfLoanBrokerID] =
"FF924CD18A236C2B49CF8E80A351CEAC6A10171DC9F110025646894FECF83F"
"5C";
txJson[sfPrincipalRequested] = "100000000";
txJson[sfStartDate] = 807730340;
txJson[sfPaymentTotal] = 10000;
txJson[sfPaymentInterval] = 3600;
txJson[sfGracePeriod] = 300;
txJson[sfFlags] = 65536; // tfLoanOverpayment
txJson[sfFee] = "24";
// 2. Borrower signs the transaction
auto const borrowerSignParams = [&]() {
Json::Value params{Json::objectValue};
params[jss::passphrase] = borrowerPass;
params[jss::key_type] = "ed25519";
params[jss::tx_json] = txJson;
return params;
}();
auto const jSignBorrower =
env.rpc("json", "sign", to_string(borrowerSignParams));
BEAST_EXPECT(
jSignBorrower.isMember(jss::result) &&
jSignBorrower[jss::result].isMember(jss::tx_json));
auto const txBorrowerSignResult =
jSignBorrower[jss::result][jss::tx_json];
auto const txBorrowerSignBlob =
jSignBorrower[jss::result][jss::tx_blob].asString();
// 2a. Borrower attempts to submit the transaction. It doesn't work
{
auto const jSubmitBlob = env.rpc("submit", txBorrowerSignBlob);
BEAST_EXPECT(jSubmitBlob.isMember(jss::result));
auto const jSubmitBlobResult = jSubmitBlob[jss::result];
BEAST_EXPECT(jSubmitBlobResult.isMember(jss::tx_json));
// Transaction fails because the CounterpartySignature is
// missing
BEAST_EXPECT(
jSubmitBlobResult.isMember(jss::engine_result) &&
jSubmitBlobResult[jss::engine_result].asString() ==
"temBAD_SIGNER");
}
// 3. Borrower sends the signed transaction to the lender
// 4. Lender signs the transaction
auto const lenderSignParams = [&]() {
Json::Value params{Json::objectValue};
params[jss::passphrase] = lenderPass;
params[jss::key_type] = "ed25519";
params[jss::signature_target] = "CounterpartySignature";
params[jss::tx_json] = txBorrowerSignResult;
return params;
}();
auto const jSignLender =
env.rpc("json", "sign", to_string(lenderSignParams));
BEAST_EXPECT(
jSignLender.isMember(jss::result) &&
jSignLender[jss::result].isMember(jss::tx_json));
auto const txLenderSignResult =
jSignLender[jss::result][jss::tx_json];
auto const txLenderSignBlob =
jSignLender[jss::result][jss::tx_blob].asString();
// 5. Lender submits the signed transaction blob
auto const jSubmitBlob = env.rpc("submit", txLenderSignBlob);
BEAST_EXPECT(jSubmitBlob.isMember(jss::result));
auto const jSubmitBlobResult = jSubmitBlob[jss::result];
BEAST_EXPECT(jSubmitBlobResult.isMember(jss::tx_json));
auto const jSubmitBlobTx = jSubmitBlobResult[jss::tx_json];
// To get far enough to return tecNO_ENTRY means that the signatures
// all validated. Of course the transaction won't succeed because no
// Vault or Broker were created.
BEAST_EXPECT(
jSubmitBlobResult.isMember(jss::engine_result) &&
jSubmitBlobResult[jss::engine_result].asString() ==
"tecNO_ENTRY");
BEAST_EXPECT( BEAST_EXPECT(
jr.isMember(jss::result) && !jSubmitBlob.isMember(jss::error) &&
jr[jss::result].isMember(jss::tx_json)); !jSubmitBlobResult.isMember(jss::error));
auto const& tx = jr[jss::result][jss::tx_json];
BEAST_EXPECT(!tx.isMember(jss::CounterpartySignature)); // 4-alt. Lender submits the transaction json originally received
// from the Borrower. It gets signed, but is now a duplicate, so
// fails. Borrower could done this instead of steps 4 and 5.
auto const jSubmitJson =
env.rpc("json", "submit", to_string(lenderSignParams));
BEAST_EXPECT(jSubmitJson.isMember(jss::result));
auto const jSubmitJsonResult = jSubmitJson[jss::result];
BEAST_EXPECT(jSubmitJsonResult.isMember(jss::tx_json));
auto const jSubmitJsonTx = jSubmitJsonResult[jss::tx_json];
// Since the previous tx claimed a fee, this duplicate is not going
// anywhere
BEAST_EXPECT( BEAST_EXPECT(
tx.isMember(jss::TxnSignature) && jSubmitJsonResult.isMember(jss::engine_result) &&
tx[jss::TxnSignature].asString().length() == 142); jSubmitJsonResult[jss::engine_result].asString() ==
"tefPAST_SEQ");
BEAST_EXPECT(
!jSubmitJson.isMember(jss::error) &&
!jSubmitJsonResult.isMember(jss::error));
BEAST_EXPECT(jSubmitBlobTx == jSubmitJsonTx);
}
{
testcase("RPC LoanSet - sign and submit lender initiated");
// 1. Lender creates the transaction
Json::Value txJson{Json::objectValue};
txJson[sfTransactionType] = "LoanSet";
txJson[sfAccount] = lender.human();
txJson[sfCounterparty] = borrower.human();
txJson[sfLoanBrokerID] =
"FF924CD18A236C2B49CF8E80A351CEAC6A10171DC9F110025646894FECF83F"
"5C";
txJson[sfPrincipalRequested] = "100000000";
txJson[sfStartDate] = 807730340;
txJson[sfPaymentTotal] = 10000;
txJson[sfPaymentInterval] = 3600;
txJson[sfGracePeriod] = 300;
txJson[sfFlags] = 65536; // tfLoanOverpayment
txJson[sfFee] = "24";
// 2. Lender signs the transaction
auto const lenderSignParams = [&]() {
Json::Value params{Json::objectValue};
params[jss::passphrase] = lenderPass;
params[jss::key_type] = "ed25519";
params[jss::tx_json] = txJson;
return params;
}();
auto const jSignLender =
env.rpc("json", "sign", to_string(lenderSignParams));
BEAST_EXPECT(
jSignLender.isMember(jss::result) &&
jSignLender[jss::result].isMember(jss::tx_json));
auto const txLenderSignResult =
jSignLender[jss::result][jss::tx_json];
auto const txLenderSignBlob =
jSignLender[jss::result][jss::tx_blob].asString();
// 2a. Lender attempts to submit the transaction. It doesn't work
{
auto const jSubmitBlob = env.rpc("submit", txLenderSignBlob);
BEAST_EXPECT(jSubmitBlob.isMember(jss::result));
auto const jSubmitBlobResult = jSubmitBlob[jss::result];
BEAST_EXPECT(jSubmitBlobResult.isMember(jss::tx_json));
// Transaction fails because the CounterpartySignature is
// missing
BEAST_EXPECT(
jSubmitBlobResult.isMember(jss::engine_result) &&
jSubmitBlobResult[jss::engine_result].asString() ==
"temBAD_SIGNER");
}
// 3. Lender sends the signed transaction to the Borrower
// 4. Borrower signs the transaction
auto const borrowerSignParams = [&]() {
Json::Value params{Json::objectValue};
params[jss::passphrase] = borrowerPass;
params[jss::key_type] = "ed25519";
params[jss::signature_target] = "CounterpartySignature";
params[jss::tx_json] = txLenderSignResult;
return params;
}();
auto const jSignBorrower =
env.rpc("json", "sign", to_string(borrowerSignParams));
BEAST_EXPECT(
jSignBorrower.isMember(jss::result) &&
jSignBorrower[jss::result].isMember(jss::tx_json));
auto const txBorrowerSignResult =
jSignBorrower[jss::result][jss::tx_json];
auto const txBorrowerSignBlob =
jSignBorrower[jss::result][jss::tx_blob].asString();
// 5. Borrower submits the signed transaction blob
auto const jSubmitBlob = env.rpc("submit", txBorrowerSignBlob);
BEAST_EXPECT(jSubmitBlob.isMember(jss::result));
auto const jSubmitBlobResult = jSubmitBlob[jss::result];
BEAST_EXPECT(jSubmitBlobResult.isMember(jss::tx_json));
auto const jSubmitBlobTx = jSubmitBlobResult[jss::tx_json];
// To get far enough to return tecNO_ENTRY means that the signatures
// all validated. Of course the transaction won't succeed because no
// Vault or Broker were created.
BEAST_EXPECT(
jSubmitBlobResult.isMember(jss::engine_result) &&
jSubmitBlobResult[jss::engine_result].asString() ==
"tecNO_ENTRY");
BEAST_EXPECT(
!jSubmitBlob.isMember(jss::error) &&
!jSubmitBlobResult.isMember(jss::error));
// 4-alt. Borrower submits the transaction json originally received
// from the Lender. It gets signed, but is now a duplicate, so
// fails. Lender could done this instead of steps 4 and 5.
auto const jSubmitJson =
env.rpc("json", "submit", to_string(borrowerSignParams));
BEAST_EXPECT(jSubmitJson.isMember(jss::result));
auto const jSubmitJsonResult = jSubmitJson[jss::result];
BEAST_EXPECT(jSubmitJsonResult.isMember(jss::tx_json));
auto const jSubmitJsonTx = jSubmitJsonResult[jss::tx_json];
// Since the previous tx claimed a fee, this duplicate is not going
// anywhere
BEAST_EXPECT(
jSubmitJsonResult.isMember(jss::engine_result) &&
jSubmitJsonResult[jss::engine_result].asString() ==
"tefPAST_SEQ");
BEAST_EXPECT(
!jSubmitJson.isMember(jss::error) &&
!jSubmitJsonResult.isMember(jss::error));
BEAST_EXPECT(jSubmitBlobTx == jSubmitJsonTx);
} }
} }

View File

@@ -168,7 +168,7 @@ printHelp(po::options_description const& desc)
" server_state [counters]\n" " server_state [counters]\n"
" sign <private_key> <tx_json> [offline]\n" " sign <private_key> <tx_json> [offline]\n"
" sign_for <signer_address> <signer_private_key> <tx_json> " " sign_for <signer_address> <signer_private_key> <tx_json> "
"[offline]\n" "[offline] [<signature_field>]\n"
" stop\n" " stop\n"
" simulate [<tx_blob>|<tx_json>] [<binary>]\n" " simulate [<tx_blob>|<tx_json>] [<binary>]\n"
" submit <tx_blob>|[<private_key> <tx_json>]\n" " submit <tx_blob>|[<private_key> <tx_json>]\n"

View File

@@ -966,7 +966,16 @@ private:
Json::Value txJSON; Json::Value txJSON;
Json::Reader reader; Json::Reader reader;
bool const bOffline = bool const bOffline =
3 == jvParams.size() && jvParams[2u].asString() == "offline"; jvParams.size() >= 3 && jvParams[2u].asString() == "offline";
std::optional<std::string> const field =
[&jvParams, bOffline]() -> std::optional<std::string> {
if (jvParams.size() < 3)
return std::nullopt;
if (jvParams.size() < 4 && bOffline)
return std::nullopt;
Json::UInt index = bOffline ? 3u : 2u;
return jvParams[index].asString();
}();
if (1 == jvParams.size()) if (1 == jvParams.size())
{ {
@@ -979,7 +988,7 @@ private:
return jvRequest; return jvRequest;
} }
else if ( else if (
(2 == jvParams.size() || bOffline) && (jvParams.size() >= 2 || bOffline) &&
reader.parse(jvParams[1u].asString(), txJSON)) reader.parse(jvParams[1u].asString(), txJSON))
{ {
// Signing or submitting tx_json. // Signing or submitting tx_json.
@@ -991,6 +1000,9 @@ private:
if (bOffline) if (bOffline)
jvRequest[jss::offline] = true; jvRequest[jss::offline] = true;
if (field)
jvRequest[jss::signature_target] = *field;
return jvRequest; return jvRequest;
} }

View File

@@ -33,6 +33,7 @@
#include <xrpl/basics/mulDiv.h> #include <xrpl/basics/mulDiv.h>
#include <xrpl/json/json_writer.h> #include <xrpl/json/json_writer.h>
#include <xrpl/protocol/ErrorCodes.h> #include <xrpl/protocol/ErrorCodes.h>
#include <xrpl/protocol/InnerObjectFormats.h>
#include <xrpl/protocol/RPCErr.h> #include <xrpl/protocol/RPCErr.h>
#include <xrpl/protocol/STParsedJSON.h> #include <xrpl/protocol/STParsedJSON.h>
#include <xrpl/protocol/Sign.h> #include <xrpl/protocol/Sign.h>
@@ -54,6 +55,7 @@ private:
AccountID const* const multiSigningAcctID_; AccountID const* const multiSigningAcctID_;
std::optional<PublicKey> multiSignPublicKey_; std::optional<PublicKey> multiSignPublicKey_;
Buffer multiSignature_; Buffer multiSignature_;
std::optional<std::reference_wrapper<SField const>> signatureTarget_;
public: public:
explicit SigningForParams() : multiSigningAcctID_(nullptr) explicit SigningForParams() : multiSigningAcctID_(nullptr)
@@ -116,12 +118,25 @@ public:
return multiSignature_; return multiSignature_;
} }
std::optional<std::reference_wrapper<SField const>> const&
getSignatureTarget() const
{
return signatureTarget_;
}
void void
setPublicKey(PublicKey const& multiSignPublicKey) setPublicKey(PublicKey const& multiSignPublicKey)
{ {
multiSignPublicKey_ = multiSignPublicKey; multiSignPublicKey_ = multiSignPublicKey;
} }
void
setSignatureTarget(
std::optional<std::reference_wrapper<SField const>> const& field)
{
signatureTarget_ = field;
}
void void
moveMultiSignature(Buffer&& multiSignature) moveMultiSignature(Buffer&& multiSignature)
{ {
@@ -427,6 +442,29 @@ transactionPreProcessImpl(
bool const verify = bool const verify =
!(params.isMember(jss::offline) && params[jss::offline].asBool()); !(params.isMember(jss::offline) && params[jss::offline].asBool());
auto const signatureTarget =
[&params]() -> std::optional<std::reference_wrapper<SField const>> {
if (params.isMember(jss::signature_target))
return SField::getField(params[jss::signature_target].asString());
return std::nullopt;
}();
// Make sure the signature target field is valid, if specified, and save the
// template for use later
auto const signatureTemplate = signatureTarget
? InnerObjectFormats::getInstance().findSOTemplateBySField(
*signatureTarget)
: nullptr;
if (signatureTarget)
{
if (!signatureTemplate)
{ // Invalid target field
return RPC::make_error(
rpcINVALID_PARAMS, signatureTarget->get().getName());
}
signingArgs.setSignatureTarget(signatureTarget);
}
if (!params.isMember(jss::tx_json)) if (!params.isMember(jss::tx_json))
return RPC::missing_field_error(jss::tx_json); return RPC::missing_field_error(jss::tx_json);
@@ -541,9 +579,10 @@ transactionPreProcessImpl(
JLOG(j.trace()) << "verify: " << toBase58(calcAccountID(pk)) << " : " JLOG(j.trace()) << "verify: " << toBase58(calcAccountID(pk)) << " : "
<< toBase58(srcAddressID); << toBase58(srcAddressID);
// Don't do this test if multisigning since the account and secret // Don't do this test if multisigning or if the signature is going into
// probably don't belong together in that case. // an alternate field since the account and secret probably don't belong
if (!signingArgs.isMultiSigning()) // together in that case.
if (!signingArgs.isMultiSigning() && !signatureTarget)
{ {
// Make sure the account and secret belong together. // Make sure the account and secret belong together.
if (tx_json.isMember(sfDelegate.jsonName)) if (tx_json.isMember(sfDelegate.jsonName))
@@ -598,7 +637,17 @@ transactionPreProcessImpl(
{ {
// If we're generating a multi-signature the SigningPubKey must be // If we're generating a multi-signature the SigningPubKey must be
// empty, otherwise it must be the master account's public key. // empty, otherwise it must be the master account's public key.
parsed.object->setFieldVL( STObject* sigObject = &*parsed.object;
if (signatureTarget)
{
// If the target object doesn't exist, make one.
if (!parsed.object->isFieldPresent(*signatureTarget))
parsed.object->setFieldObject(
*signatureTarget,
STObject{*signatureTemplate, *signatureTarget});
sigObject = &parsed.object->peekFieldObject(*signatureTarget);
}
sigObject->setFieldVL(
sfSigningPubKey, sfSigningPubKey,
signingArgs.isMultiSigning() ? Slice(nullptr, 0) : pk.slice()); signingArgs.isMultiSigning() ? Slice(nullptr, 0) : pk.slice());
@@ -630,7 +679,7 @@ transactionPreProcessImpl(
} }
else if (signingArgs.isSingleSigning()) else if (signingArgs.isSingleSigning())
{ {
stTx->sign(pk, sk); stTx->sign(pk, sk, signatureTarget);
} }
return transactionPreProcessResult{std::move(stTx)}; return transactionPreProcessResult{std::move(stTx)};
@@ -1195,11 +1244,17 @@ transactionSignFor(
signer.setFieldVL( signer.setFieldVL(
sfSigningPubKey, signForParams.getPublicKey().slice()); sfSigningPubKey, signForParams.getPublicKey().slice());
STObject& sigTarget = [&]() -> STObject& {
auto const target = signForParams.getSignatureTarget();
if (target)
return sttx->peekFieldObject(*target);
return *sttx;
}();
// If there is not yet a Signers array, make one. // If there is not yet a Signers array, make one.
if (!sttx->isFieldPresent(sfSigners)) if (!sigTarget.isFieldPresent(sfSigners))
sttx->setFieldArray(sfSigners, {}); sigTarget.setFieldArray(sfSigners, {});
auto& signers = sttx->peekFieldArray(sfSigners); auto& signers = sigTarget.peekFieldArray(sfSigners);
signers.emplace_back(std::move(signer)); signers.emplace_back(std::move(signer));
// The array must be sorted and validated. // The array must be sorted and validated.