From 4840cf2ee0cc9e80fcd761a8579511bae1b77b1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=CC=81=20A=2EP?= <53834183+Jossec101@users.noreply.github.com> Date: Mon, 5 Oct 2026 12:46:45 +0200 Subject: [PATCH 1/4] Pin PSBT signer output byte-for-byte before dependency changes Signatures are deterministic (RFC 6979), so the six existing signing vectors get golden outputs recorded with NBitcoin 10.0.9. Any later bump or refactor must reproduce them exactly. Tests that set MF_ env vars share one xUnit collection so they don't race. --- .../Fixtures/golden-psbt-signatures.json | 44 +++++++++++ RemoteSigner.Tests/FunctionTest.cs | 1 + RemoteSigner.Tests/GoldenSignatureTests.cs | 73 +++++++++++++++++++ RemoteSigner.Tests/RemoteSigner.Tests.csproj | 3 + 4 files changed, 121 insertions(+) create mode 100644 RemoteSigner.Tests/Fixtures/golden-psbt-signatures.json create mode 100644 RemoteSigner.Tests/GoldenSignatureTests.cs diff --git a/RemoteSigner.Tests/Fixtures/golden-psbt-signatures.json b/RemoteSigner.Tests/Fixtures/golden-psbt-signatures.json new file mode 100644 index 0000000..2b17b16 --- /dev/null +++ b/RemoteSigner.Tests/Fixtures/golden-psbt-signatures.json @@ -0,0 +1,44 @@ +[ + { + "name": "p2wsh-2of3-all", + "psbt": "cHNidP8BAF4BAAAAAcbYkt1iwOa6IsI8lrNx1DWQCCg/y7\u002BfQTlfEhDIKOWVAAAAAAD/////AeiN9QUAAAAAIgAgg\u002BaofANl6wKKByTgFl5yBnqUK8f7sn4ULhAAIJb1C0cAAAAATwEENYfPAy8RJCyAAAAB/DvuQjoBjOttImoGYyiO0Pte4PqdeQqzcNAw4Ecw5sgDgI4uHNSCvdBxlpQ8WoEz0WmvhgIra7A4F3FkTsB0RNcQH8zk3jAAAIABAACAAQAAgE8BBDWHzwNWrAP0gAAAAfkIrkpmsP\u002BhqxS1WvDOSPKnAiXLkBCQLWkBr5C5Po\u002BBAlGvFeBbuLfqwYlbP19H/\u002B/s2DIaAu8iKY\u002BJ0KIDffBgEGDzoLMwAACAAQAAgAEAAIBPAQQ1h88DfblGjYAAAAH1InDHaHo6\u002BzUe9PG5owwQ87bTkhcGg66pSIwTmhHJmAMiI4UjOOpn\u002B/2Nw1KrJiXnmid2RiEja/HAITCQ00ienxDtAhDIMAAAgAEAAIABAACAAAEBK2SQ9QUAAAAAIgAguNLINpkV//IIFd1ti2ig15\u002B6mPOhNWykV0mwsneO9FciAgMnQqNaMT2Yz47ME\u002BCqhsEMK9fB1sQRGvbBQkPau524BkcwRAIgPcwj6yaA6RZn\u002B4YSHi4S1WE5ziHEt0IZO5KqDE5B0zMCID6cSLumR2AbgwqMTI3/Z3szEyMQauxtzvBpY8Z4oSp8AgEDBAIAAAABBWlSIQMnQqNaMT2Yz47ME\u002BCqhsEMK9fB1sQRGvbBQkPau524BiEDgTQLkhqca3brBTunNmjIsb4WEsFryTwd3BH/ZPS4KkohA91uD9EYRlzIBT6yNU2S2L/wvOA0/em4ocaM//veOtN2U64iBgMnQqNaMT2Yz47ME\u002BCqhsEMK9fB1sQRGvbBQkPau524BhgfzOTeMAAAgAEAAIABAACAAQAAAAAAAAAiBgOBNAuSGpxrdusFO6c2aMixvhYSwWvJPB3cEf9k9LgqShjtAhDIMAAAgAEAAIABAACAAQAAAAAAAAAiBgPdbg/RGEZcyAU\u002BsjVNkti/8LzgNP3puKHGjP/73jrTdhhg86CzMAAAgAEAAIABAACAAQAAAAAAAAAAAA==", + "sigHash": "All", + "network": "Regtest", + "expected": "cHNidP8BAF4BAAAAAcbYkt1iwOa6IsI8lrNx1DWQCCg/y7\u002BfQTlfEhDIKOWVAAAAAAD/////AeiN9QUAAAAAIgAgg\u002BaofANl6wKKByTgFl5yBnqUK8f7sn4ULhAAIJb1C0cAAAAATwEENYfPAy8RJCyAAAAB/DvuQjoBjOttImoGYyiO0Pte4PqdeQqzcNAw4Ecw5sgDgI4uHNSCvdBxlpQ8WoEz0WmvhgIra7A4F3FkTsB0RNcQH8zk3jAAAIABAACAAQAAgE8BBDWHzwNWrAP0gAAAAfkIrkpmsP\u002BhqxS1WvDOSPKnAiXLkBCQLWkBr5C5Po\u002BBAlGvFeBbuLfqwYlbP19H/\u002B/s2DIaAu8iKY\u002BJ0KIDffBgEGDzoLMwAACAAQAAgAEAAIBPAQQ1h88DfblGjYAAAAH1InDHaHo6\u002BzUe9PG5owwQ87bTkhcGg66pSIwTmhHJmAMiI4UjOOpn\u002B/2Nw1KrJiXnmid2RiEja/HAITCQ00ienxDtAhDIMAAAgAEAAIABAACAAAEBK2SQ9QUAAAAAIgAguNLINpkV//IIFd1ti2ig15\u002B6mPOhNWykV0mwsneO9FciAgMnQqNaMT2Yz47ME\u002BCqhsEMK9fB1sQRGvbBQkPau524BkcwRAIgPcwj6yaA6RZn\u002B4YSHi4S1WE5ziHEt0IZO5KqDE5B0zMCID6cSLumR2AbgwqMTI3/Z3szEyMQauxtzvBpY8Z4oSp8AiICA4E0C5IanGt26wU7pzZoyLG\u002BFhLBa8k8HdwR/2T0uCpKRzBEAiBFGR\u002BvauPZsjnrTMioqL3pSpWJwNdLxl4O84IrB/NajgIge3JAi2Yynykj2USpjdTQu2/Da/Vv2FuvG20RKrd/\u002B3ABAQMEAQAAAAEFaVIhAydCo1oxPZjPjswT4KqGwQwr18HWxBEa9sFCQ9q7nbgGIQOBNAuSGpxrdusFO6c2aMixvhYSwWvJPB3cEf9k9LgqSiED3W4P0RhGXMgFPrI1TZLYv/C84DT96bihxoz/\u002B94603ZTriIGAydCo1oxPZjPjswT4KqGwQwr18HWxBEa9sFCQ9q7nbgGGB/M5N4wAACAAQAAgAEAAIABAAAAAAAAACIGA4E0C5IanGt26wU7pzZoyLG\u002BFhLBa8k8HdwR/2T0uCpKGO0CEMgwAACAAQAAgAEAAIABAAAAAAAAACIGA91uD9EYRlzIBT6yNU2S2L/wvOA0/em4ocaM//veOtN2GGDzoLMwAACAAQAAgAEAAIABAAAAAAAAAAAA" + }, + { + "name": "p2wsh-2of3-none", + "psbt": "cHNidP8BAF4BAAAAAcbYkt1iwOa6IsI8lrNx1DWQCCg/y7\u002BfQTlfEhDIKOWVAAAAAAD/////AeiN9QUAAAAAIgAgg\u002BaofANl6wKKByTgFl5yBnqUK8f7sn4ULhAAIJb1C0cAAAAATwEENYfPAy8RJCyAAAAB/DvuQjoBjOttImoGYyiO0Pte4PqdeQqzcNAw4Ecw5sgDgI4uHNSCvdBxlpQ8WoEz0WmvhgIra7A4F3FkTsB0RNcQH8zk3jAAAIABAACAAQAAgE8BBDWHzwNWrAP0gAAAAfkIrkpmsP\u002BhqxS1WvDOSPKnAiXLkBCQLWkBr5C5Po\u002BBAlGvFeBbuLfqwYlbP19H/\u002B/s2DIaAu8iKY\u002BJ0KIDffBgEGDzoLMwAACAAQAAgAEAAIBPAQQ1h88DfblGjYAAAAH1InDHaHo6\u002BzUe9PG5owwQ87bTkhcGg66pSIwTmhHJmAMiI4UjOOpn\u002B/2Nw1KrJiXnmid2RiEja/HAITCQ00ienxDtAhDIMAAAgAEAAIABAACAAAEBK2SQ9QUAAAAAIgAguNLINpkV//IIFd1ti2ig15\u002B6mPOhNWykV0mwsneO9FciAgMnQqNaMT2Yz47ME\u002BCqhsEMK9fB1sQRGvbBQkPau524BkcwRAIgPcwj6yaA6RZn\u002B4YSHi4S1WE5ziHEt0IZO5KqDE5B0zMCID6cSLumR2AbgwqMTI3/Z3szEyMQauxtzvBpY8Z4oSp8AgEDBAIAAAABBWlSIQMnQqNaMT2Yz47ME\u002BCqhsEMK9fB1sQRGvbBQkPau524BiEDgTQLkhqca3brBTunNmjIsb4WEsFryTwd3BH/ZPS4KkohA91uD9EYRlzIBT6yNU2S2L/wvOA0/em4ocaM//veOtN2U64iBgMnQqNaMT2Yz47ME\u002BCqhsEMK9fB1sQRGvbBQkPau524BhgfzOTeMAAAgAEAAIABAACAAQAAAAAAAAAiBgOBNAuSGpxrdusFO6c2aMixvhYSwWvJPB3cEf9k9LgqShjtAhDIMAAAgAEAAIABAACAAQAAAAAAAAAiBgPdbg/RGEZcyAU\u002BsjVNkti/8LzgNP3puKHGjP/73jrTdhhg86CzMAAAgAEAAIABAACAAQAAAAAAAAAAAA==", + "sigHash": "None", + "network": "Regtest", + "expected": "cHNidP8BAF4BAAAAAcbYkt1iwOa6IsI8lrNx1DWQCCg/y7\u002BfQTlfEhDIKOWVAAAAAAD/////AeiN9QUAAAAAIgAgg\u002BaofANl6wKKByTgFl5yBnqUK8f7sn4ULhAAIJb1C0cAAAAATwEENYfPAy8RJCyAAAAB/DvuQjoBjOttImoGYyiO0Pte4PqdeQqzcNAw4Ecw5sgDgI4uHNSCvdBxlpQ8WoEz0WmvhgIra7A4F3FkTsB0RNcQH8zk3jAAAIABAACAAQAAgE8BBDWHzwNWrAP0gAAAAfkIrkpmsP\u002BhqxS1WvDOSPKnAiXLkBCQLWkBr5C5Po\u002BBAlGvFeBbuLfqwYlbP19H/\u002B/s2DIaAu8iKY\u002BJ0KIDffBgEGDzoLMwAACAAQAAgAEAAIBPAQQ1h88DfblGjYAAAAH1InDHaHo6\u002BzUe9PG5owwQ87bTkhcGg66pSIwTmhHJmAMiI4UjOOpn\u002B/2Nw1KrJiXnmid2RiEja/HAITCQ00ienxDtAhDIMAAAgAEAAIABAACAAAEBK2SQ9QUAAAAAIgAguNLINpkV//IIFd1ti2ig15\u002B6mPOhNWykV0mwsneO9FciAgMnQqNaMT2Yz47ME\u002BCqhsEMK9fB1sQRGvbBQkPau524BkcwRAIgPcwj6yaA6RZn\u002B4YSHi4S1WE5ziHEt0IZO5KqDE5B0zMCID6cSLumR2AbgwqMTI3/Z3szEyMQauxtzvBpY8Z4oSp8AiICA4E0C5IanGt26wU7pzZoyLG\u002BFhLBa8k8HdwR/2T0uCpKRzBEAiBFGR\u002BvauPZsjnrTMioqL3pSpWJwNdLxl4O84IrB/NajgIge3JAi2Yynykj2USpjdTQu2/Da/Vv2FuvG20RKrd/\u002B3ABAQMEAgAAAAEFaVIhAydCo1oxPZjPjswT4KqGwQwr18HWxBEa9sFCQ9q7nbgGIQOBNAuSGpxrdusFO6c2aMixvhYSwWvJPB3cEf9k9LgqSiED3W4P0RhGXMgFPrI1TZLYv/C84DT96bihxoz/\u002B94603ZTriIGAydCo1oxPZjPjswT4KqGwQwr18HWxBEa9sFCQ9q7nbgGGB/M5N4wAACAAQAAgAEAAIABAAAAAAAAACIGA4E0C5IanGt26wU7pzZoyLG\u002BFhLBa8k8HdwR/2T0uCpKGO0CEMgwAACAAQAAgAEAAIABAAAAAAAAACIGA91uD9EYRlzIBT6yNU2S2L/wvOA0/em4ocaM//veOtN2GGDzoLMwAACAAQAAgAEAAIABAAAAAAAAAAAA" + }, + { + "name": "p2wpkh-3in-all", + "psbt": "cHNidP8BAKQBAAAAAwXAGAr1uq/i06r\u002BEW2SjFMKQp3Pg0q\u002BeJcqQ9iWKLrMAAAAAAD/////E9fa5RGuwTHL6xLgYpdDDXz2piFg7F9UWPZXyAZdM8kAAAAAAP////9YszwapNpRRrI7LFJglswjr9SLkao\u002BywZq/AtjZMDChAAAAAAA/////wGsJgMGAAAAABYAFJO3OqgJq4Mr3qWsDV1YUNj0aDHQAAAAAE8BBDWHzwN9uUaNAAAAAIDetxqi8U7tfzci9EleGtB59Z/A84PlsnvZ229emSEgA6/rPqXCpw3EqihylkpeL/EXKvNGahv\u002B0Dm2JmVJf8VGEO0CEMgwAACAAQAAgAAAAAAAAQEfjGkeAAAAAAAWABQCmza03sKejExNXjBVHR8UyJJWpgEDBAIAAAAiBgIUCFqogmf/kpcaV\u002B42XlzRzx4OWdqxWDesHZkVuK70sBjtAhDIMAAAgAEAAIAAAAAAAAAAAD0AAAAAAQEfyFrXBQAAAAAWABR9cTsoys8smwP2qmjSQM06tKj4fwEDBAIAAAAiBgPzpHxMZtZ1f3rW4L0yyV4gPS45MGMDooXHpvIhAGbvtBjtAhDIMAAAgAEAAIAAAAAAAQAAAC8AAAAAAQEfMGYNAAAAAAAWABRmqBq5qDk2/37GDEq0zM5HXigXjwEDBAIAAAAiBgJr4vl26F2PI9F3JT63vX1qltyDoaAOZ/D212UNJ3u1XhjtAhDIMAAAgAEAAIAAAAAAAQAAADQAAAAAAA==", + "sigHash": "All", + "network": "Regtest", + "expected": "cHNidP8BAKQBAAAAAwXAGAr1uq/i06r\u002BEW2SjFMKQp3Pg0q\u002BeJcqQ9iWKLrMAAAAAAD/////E9fa5RGuwTHL6xLgYpdDDXz2piFg7F9UWPZXyAZdM8kAAAAAAP////9YszwapNpRRrI7LFJglswjr9SLkao\u002BywZq/AtjZMDChAAAAAAA/////wGsJgMGAAAAABYAFJO3OqgJq4Mr3qWsDV1YUNj0aDHQAAAAAE8BBDWHzwN9uUaNAAAAAIDetxqi8U7tfzci9EleGtB59Z/A84PlsnvZ229emSEgA6/rPqXCpw3EqihylkpeL/EXKvNGahv\u002B0Dm2JmVJf8VGEO0CEMgwAACAAQAAgAAAAAAAAQEfjGkeAAAAAAAWABQCmza03sKejExNXjBVHR8UyJJWpiICAhQIWqiCZ/\u002BSlxpX7jZeXNHPHg5Z2rFYN6wdmRW4rvSwRzBEAiBuE2ST1E17NZdYPAV/4UA5rextY1RdM\u002BGKHXNR\u002BBJOwQIgSDC/XEww6tjC0n9/9\u002BMJlZVtOQGAEMJamIG1fSXgt4kBAQMEAQAAACIGAhQIWqiCZ/\u002BSlxpX7jZeXNHPHg5Z2rFYN6wdmRW4rvSwGO0CEMgwAACAAQAAgAAAAAAAAAAAPQAAAAABAR/IWtcFAAAAABYAFH1xOyjKzyybA/aqaNJAzTq0qPh/IgID86R8TGbWdX961uC9MsleID0uOTBjA6KFx6byIQBm77RHMEQCIGegAS50pocdDBKqq9A7CzQ9L8tIZ6d49KIji5uWDUesAiBv0rzp\u002BzbKX4Nv9ERH5LzqoLv6Nsu/qZEMozwmblwWtgEBAwQBAAAAIgYD86R8TGbWdX961uC9MsleID0uOTBjA6KFx6byIQBm77QY7QIQyDAAAIABAACAAAAAAAEAAAAvAAAAAAEBHzBmDQAAAAAAFgAUZqgauag5Nv9\u002BxgxKtMzOR14oF48iAgJr4vl26F2PI9F3JT63vX1qltyDoaAOZ/D212UNJ3u1XkcwRAIgAy3\u002Bo\u002BcTPwG77GTmDGEXmCLZ6Yb\u002BLh8w5yEvDi6uFsoCIE7T/74Wa4Dsc1JpIsQtoJd5N7BiMtCAape40rBFTjz3AQEDBAEAAAAiBgJr4vl26F2PI9F3JT63vX1qltyDoaAOZ/D212UNJ3u1XhjtAhDIMAAAgAEAAIAAAAAAAQAAADQAAAAAAA==" + }, + { + "name": "p2wpkh-3in-none", + "psbt": "cHNidP8BAKQBAAAAAwXAGAr1uq/i06r\u002BEW2SjFMKQp3Pg0q\u002BeJcqQ9iWKLrMAAAAAAD/////E9fa5RGuwTHL6xLgYpdDDXz2piFg7F9UWPZXyAZdM8kAAAAAAP////9YszwapNpRRrI7LFJglswjr9SLkao\u002BywZq/AtjZMDChAAAAAAA/////wGsJgMGAAAAABYAFJO3OqgJq4Mr3qWsDV1YUNj0aDHQAAAAAE8BBDWHzwN9uUaNAAAAAIDetxqi8U7tfzci9EleGtB59Z/A84PlsnvZ229emSEgA6/rPqXCpw3EqihylkpeL/EXKvNGahv\u002B0Dm2JmVJf8VGEO0CEMgwAACAAQAAgAAAAAAAAQEfjGkeAAAAAAAWABQCmza03sKejExNXjBVHR8UyJJWpgEDBAIAAAAiBgIUCFqogmf/kpcaV\u002B42XlzRzx4OWdqxWDesHZkVuK70sBjtAhDIMAAAgAEAAIAAAAAAAAAAAD0AAAAAAQEfyFrXBQAAAAAWABR9cTsoys8smwP2qmjSQM06tKj4fwEDBAIAAAAiBgPzpHxMZtZ1f3rW4L0yyV4gPS45MGMDooXHpvIhAGbvtBjtAhDIMAAAgAEAAIAAAAAAAQAAAC8AAAAAAQEfMGYNAAAAAAAWABRmqBq5qDk2/37GDEq0zM5HXigXjwEDBAIAAAAiBgJr4vl26F2PI9F3JT63vX1qltyDoaAOZ/D212UNJ3u1XhjtAhDIMAAAgAEAAIAAAAAAAQAAADQAAAAAAA==", + "sigHash": "None", + "network": "Regtest", + "expected": "cHNidP8BAKQBAAAAAwXAGAr1uq/i06r\u002BEW2SjFMKQp3Pg0q\u002BeJcqQ9iWKLrMAAAAAAD/////E9fa5RGuwTHL6xLgYpdDDXz2piFg7F9UWPZXyAZdM8kAAAAAAP////9YszwapNpRRrI7LFJglswjr9SLkao\u002BywZq/AtjZMDChAAAAAAA/////wGsJgMGAAAAABYAFJO3OqgJq4Mr3qWsDV1YUNj0aDHQAAAAAE8BBDWHzwN9uUaNAAAAAIDetxqi8U7tfzci9EleGtB59Z/A84PlsnvZ229emSEgA6/rPqXCpw3EqihylkpeL/EXKvNGahv\u002B0Dm2JmVJf8VGEO0CEMgwAACAAQAAgAAAAAAAAQEfjGkeAAAAAAAWABQCmza03sKejExNXjBVHR8UyJJWpiICAhQIWqiCZ/\u002BSlxpX7jZeXNHPHg5Z2rFYN6wdmRW4rvSwRzBEAiBuE2ST1E17NZdYPAV/4UA5rextY1RdM\u002BGKHXNR\u002BBJOwQIgSDC/XEww6tjC0n9/9\u002BMJlZVtOQGAEMJamIG1fSXgt4kBAQMEAgAAACIGAhQIWqiCZ/\u002BSlxpX7jZeXNHPHg5Z2rFYN6wdmRW4rvSwGO0CEMgwAACAAQAAgAAAAAAAAAAAPQAAAAABAR/IWtcFAAAAABYAFH1xOyjKzyybA/aqaNJAzTq0qPh/IgID86R8TGbWdX961uC9MsleID0uOTBjA6KFx6byIQBm77RHMEQCIGegAS50pocdDBKqq9A7CzQ9L8tIZ6d49KIji5uWDUesAiBv0rzp\u002BzbKX4Nv9ERH5LzqoLv6Nsu/qZEMozwmblwWtgEBAwQCAAAAIgYD86R8TGbWdX961uC9MsleID0uOTBjA6KFx6byIQBm77QY7QIQyDAAAIABAACAAAAAAAEAAAAvAAAAAAEBHzBmDQAAAAAAFgAUZqgauag5Nv9\u002BxgxKtMzOR14oF48iAgJr4vl26F2PI9F3JT63vX1qltyDoaAOZ/D212UNJ3u1XkcwRAIgAy3\u002Bo\u002BcTPwG77GTmDGEXmCLZ6Yb\u002BLh8w5yEvDi6uFsoCIE7T/74Wa4Dsc1JpIsQtoJd5N7BiMtCAape40rBFTjz3AQEDBAIAAAAiBgJr4vl26F2PI9F3JT63vX1qltyDoaAOZ/D212UNJ3u1XhjtAhDIMAAAgAEAAIAAAAAAAQAAADQAAAAAAA==" + }, + { + "name": "p2wpkh-samepath-all", + "psbt": "cHNidP8BAIUBAAAAA5AuveJtsmh34KQCGn2QjTVgYZeAP0wBbFfk5ZOGpl0OAAAAAAD/////0qsBGBoCPD41OUI73G9W8Q1hv3z8MkaGRgWlTiutSFwAAAAAAP////\u002B7IC\u002BzZyJI1BBQ/ghKrJi18xK\u002BL7VVg1Q/B4ohnnMFsAEAAAAA/////wAAAAAATwEENYfPA325Ro0AAAABg9H86IDUttPPFss\u002B9te\u002B0DByQgbeD7RPXNuVH9mh1qIDnMEWyKA\u002BkvyG038on8\u002BHxI\u002B9AD8r6ZI1dNIDSGC8824Q7QIQyDAAAIABAACAAQAAAAABAR\u002BghgEAAAAAABYAFBQptc6zAas/qjlmx6MhONjyd11nAQMEAgAAACIGAvFR60eN3aNhBP2/c4cH9itYW\u002BUFAq7F1bDfFv40mrRiGO0CEMgwAACAAQAAgAEAAAAAAAAAMwAAAAABAR\u002BghgEAAAAAABYAFBQptc6zAas/qjlmx6MhONjyd11nAQMEAgAAACIGAvFR60eN3aNhBP2/c4cH9itYW\u002BUFAq7F1bDfFv40mrRiGO0CEMgwAACAAQAAgAEAAAAAAAAAMwAAAAABAR\u002BghgEAAAAAABYAFBQptc6zAas/qjlmx6MhONjyd11nAQMEAgAAACIGAvFR60eN3aNhBP2/c4cH9itYW\u002BUFAq7F1bDfFv40mrRiGO0CEMgwAACAAQAAgAEAAAAAAAAAMwAAAAA=", + "sigHash": "All", + "network": "Regtest", + "expected": "cHNidP8BAIUBAAAAA5AuveJtsmh34KQCGn2QjTVgYZeAP0wBbFfk5ZOGpl0OAAAAAAD/////0qsBGBoCPD41OUI73G9W8Q1hv3z8MkaGRgWlTiutSFwAAAAAAP////\u002B7IC\u002BzZyJI1BBQ/ghKrJi18xK\u002BL7VVg1Q/B4ohnnMFsAEAAAAA/////wAAAAAATwEENYfPA325Ro0AAAABg9H86IDUttPPFss\u002B9te\u002B0DByQgbeD7RPXNuVH9mh1qIDnMEWyKA\u002BkvyG038on8\u002BHxI\u002B9AD8r6ZI1dNIDSGC8824Q7QIQyDAAAIABAACAAQAAAAABAR\u002BghgEAAAAAABYAFBQptc6zAas/qjlmx6MhONjyd11nIgIC8VHrR43do2EE/b9zhwf2K1hb5QUCrsXVsN8W/jSatGJHMEQCICzxabvK1oB8RUMHyd5pCg5jnOaAM70BuiKN\u002BoLJhhs8AiAW6D9va0tjfvk/HccVzYx6tTnCziZZnsG4OOHAZElD1AEBAwQBAAAAIgYC8VHrR43do2EE/b9zhwf2K1hb5QUCrsXVsN8W/jSatGIY7QIQyDAAAIABAACAAQAAAAAAAAAzAAAAAAEBH6CGAQAAAAAAFgAUFCm1zrMBqz\u002BqOWbHoyE42PJ3XWciAgLxUetHjd2jYQT9v3OHB/YrWFvlBQKuxdWw3xb\u002BNJq0YkcwRAIgZ5FQCVtOe8P5tBamiWC3rXZ8vL3xmKicHWaNy2jL3ScCIEYkW4Bdhp\u002BuFkga/AiZafMYzi7y/rlA2M52rQjCjLeGAQEDBAEAAAAiBgLxUetHjd2jYQT9v3OHB/YrWFvlBQKuxdWw3xb\u002BNJq0YhjtAhDIMAAAgAEAAIABAAAAAAAAADMAAAAAAQEfoIYBAAAAAAAWABQUKbXOswGrP6o5ZsejITjY8nddZyICAvFR60eN3aNhBP2/c4cH9itYW\u002BUFAq7F1bDfFv40mrRiRzBEAiA1M\u002BMgTmvnqdi2JnE540v5E1L6jyshtwUy991Ws3FtFAIgWdjj75lK4X5EPV8SqAASzKhuPYd\u002BOxZ946FTrS\u002BqHIUBAQMEAQAAACIGAvFR60eN3aNhBP2/c4cH9itYW\u002BUFAq7F1bDfFv40mrRiGO0CEMgwAACAAQAAgAEAAAAAAAAAMwAAAAA=" + }, + { + "name": "p2wpkh-samepath-none", + "psbt": "cHNidP8BAIUBAAAAA5AuveJtsmh34KQCGn2QjTVgYZeAP0wBbFfk5ZOGpl0OAAAAAAD/////0qsBGBoCPD41OUI73G9W8Q1hv3z8MkaGRgWlTiutSFwAAAAAAP////\u002B7IC\u002BzZyJI1BBQ/ghKrJi18xK\u002BL7VVg1Q/B4ohnnMFsAEAAAAA/////wAAAAAATwEENYfPA325Ro0AAAABg9H86IDUttPPFss\u002B9te\u002B0DByQgbeD7RPXNuVH9mh1qIDnMEWyKA\u002BkvyG038on8\u002BHxI\u002B9AD8r6ZI1dNIDSGC8824Q7QIQyDAAAIABAACAAQAAAAABAR\u002BghgEAAAAAABYAFBQptc6zAas/qjlmx6MhONjyd11nAQMEAgAAACIGAvFR60eN3aNhBP2/c4cH9itYW\u002BUFAq7F1bDfFv40mrRiGO0CEMgwAACAAQAAgAEAAAAAAAAAMwAAAAABAR\u002BghgEAAAAAABYAFBQptc6zAas/qjlmx6MhONjyd11nAQMEAgAAACIGAvFR60eN3aNhBP2/c4cH9itYW\u002BUFAq7F1bDfFv40mrRiGO0CEMgwAACAAQAAgAEAAAAAAAAAMwAAAAABAR\u002BghgEAAAAAABYAFBQptc6zAas/qjlmx6MhONjyd11nAQMEAgAAACIGAvFR60eN3aNhBP2/c4cH9itYW\u002BUFAq7F1bDfFv40mrRiGO0CEMgwAACAAQAAgAEAAAAAAAAAMwAAAAA=", + "sigHash": "None", + "network": "Regtest", + "expected": "cHNidP8BAIUBAAAAA5AuveJtsmh34KQCGn2QjTVgYZeAP0wBbFfk5ZOGpl0OAAAAAAD/////0qsBGBoCPD41OUI73G9W8Q1hv3z8MkaGRgWlTiutSFwAAAAAAP////\u002B7IC\u002BzZyJI1BBQ/ghKrJi18xK\u002BL7VVg1Q/B4ohnnMFsAEAAAAA/////wAAAAAATwEENYfPA325Ro0AAAABg9H86IDUttPPFss\u002B9te\u002B0DByQgbeD7RPXNuVH9mh1qIDnMEWyKA\u002BkvyG038on8\u002BHxI\u002B9AD8r6ZI1dNIDSGC8824Q7QIQyDAAAIABAACAAQAAAAABAR\u002BghgEAAAAAABYAFBQptc6zAas/qjlmx6MhONjyd11nIgIC8VHrR43do2EE/b9zhwf2K1hb5QUCrsXVsN8W/jSatGJHMEQCICzxabvK1oB8RUMHyd5pCg5jnOaAM70BuiKN\u002BoLJhhs8AiAW6D9va0tjfvk/HccVzYx6tTnCziZZnsG4OOHAZElD1AEBAwQCAAAAIgYC8VHrR43do2EE/b9zhwf2K1hb5QUCrsXVsN8W/jSatGIY7QIQyDAAAIABAACAAQAAAAAAAAAzAAAAAAEBH6CGAQAAAAAAFgAUFCm1zrMBqz\u002BqOWbHoyE42PJ3XWciAgLxUetHjd2jYQT9v3OHB/YrWFvlBQKuxdWw3xb\u002BNJq0YkcwRAIgZ5FQCVtOe8P5tBamiWC3rXZ8vL3xmKicHWaNy2jL3ScCIEYkW4Bdhp\u002BuFkga/AiZafMYzi7y/rlA2M52rQjCjLeGAQEDBAIAAAAiBgLxUetHjd2jYQT9v3OHB/YrWFvlBQKuxdWw3xb\u002BNJq0YhjtAhDIMAAAgAEAAIABAAAAAAAAADMAAAAAAQEfoIYBAAAAAAAWABQUKbXOswGrP6o5ZsejITjY8nddZyICAvFR60eN3aNhBP2/c4cH9itYW\u002BUFAq7F1bDfFv40mrRiRzBEAiA1M\u002BMgTmvnqdi2JnE540v5E1L6jyshtwUy991Ws3FtFAIgWdjj75lK4X5EPV8SqAASzKhuPYd\u002BOxZ946FTrS\u002BqHIUBAQMEAgAAACIGAvFR60eN3aNhBP2/c4cH9itYW\u002BUFAq7F1bDfFv40mrRiGO0CEMgwAACAAQAAgAEAAAAAAAAAMwAAAAA=" + } +] diff --git a/RemoteSigner.Tests/FunctionTest.cs b/RemoteSigner.Tests/FunctionTest.cs index a3388e5..b5dd05b 100644 --- a/RemoteSigner.Tests/FunctionTest.cs +++ b/RemoteSigner.Tests/FunctionTest.cs @@ -7,6 +7,7 @@ namespace RemoteSigner.Tests; +[Collection(SignerEnvironment.Collection)] public class FunctionTest : IDisposable { private const string awsKmsKeyId = "mrk-51c7f523a83442718f2f71a986b7c7fb"; diff --git a/RemoteSigner.Tests/GoldenSignatureTests.cs b/RemoteSigner.Tests/GoldenSignatureTests.cs new file mode 100644 index 0000000..e91d73f --- /dev/null +++ b/RemoteSigner.Tests/GoldenSignatureTests.cs @@ -0,0 +1,73 @@ +using System.Text.Json; +using FluentAssertions; +using NBitcoin; +using Xunit; + +namespace RemoteSigner.Tests; + +/// +/// Byte-for-byte pins of the PSBT signer's output. ECDSA signatures are deterministic (RFC 6979), +/// so a dependency bump (NBitcoin) or a refactor of the request path must reproduce exactly these +/// PSBTs. Regenerate only on purpose: GOLDEN_UPDATE=1 dotnet test --filter GoldenSignatureTests. +/// +[Collection(SignerEnvironment.Collection)] +public class GoldenSignatureTests : IDisposable +{ + // SignPSBT only signs inputs whose fingerprint has an MF_ entry; the seed comes from the delegate. + public GoldenSignatureTests() => Environment.SetEnvironmentVariable("MF_ed0210c8", "{}"); + + public void Dispose() => Environment.SetEnvironmentVariable("MF_ed0210c8", null); + + private const string Mnemonic = + "middle teach digital prefer fiscal theory syrup enter crash muffin easily anxiety ill barely eagle swim volume consider dynamic unaware deputy middle into physical"; + + private static readonly string FixturePath = Path.Combine(AppContext.BaseDirectory, "Fixtures", "golden-psbt-signatures.json"); + + public static IEnumerable Cases() => + Load().Select(c => new object[] { c.Name }); + + [Theory] + [MemberData(nameof(Cases))] + public async Task SignPSBT_IsByteIdenticalToTheGoldenOutput(string name) + { + var all = Load(); + var golden = all.Single(c => c.Name == name); + + var result = await new Function().SignPSBT(golden.Psbt, golden.Network, Enum.Parse(golden.SigHash), + _ => Task.FromResult(Mnemonic)); + + if (Environment.GetEnvironmentVariable("GOLDEN_UPDATE") == "1") + { + golden.Expected = result!.Psbt; + var source = Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "Fixtures", "golden-psbt-signatures.json"); + var onDisk = JsonSerializer.Deserialize>(await File.ReadAllTextAsync(source), Json)!; + onDisk.Single(c => c.Name == name).Expected = result.Psbt; + await File.WriteAllTextAsync(source, JsonSerializer.Serialize(onDisk, Json) + "\n"); + return; + } + + golden.Expected.Should().NotBeNullOrEmpty("the golden output must be recorded"); + result!.Psbt.Should().Be(golden.Expected); + } + + private static readonly JsonSerializerOptions Json = new(JsonSerializerDefaults.Web) { WriteIndented = true }; + + private static List Load() => + JsonSerializer.Deserialize>(File.ReadAllText(FixturePath), Json)!; + + public sealed class GoldenCase + { + public string Name { get; set; } = string.Empty; + public string Psbt { get; set; } = string.Empty; + public string SigHash { get; set; } = string.Empty; + public string Network { get; set; } = string.Empty; + public string? Expected { get; set; } + } +} + +/// Tests that set process-wide MF_ env vars must not run in parallel with each other. +[CollectionDefinition(Collection)] +public sealed class SignerEnvironment +{ + public const string Collection = "Signer environment variables"; +} diff --git a/RemoteSigner.Tests/RemoteSigner.Tests.csproj b/RemoteSigner.Tests/RemoteSigner.Tests.csproj index 4e96afb..ce23967 100644 --- a/RemoteSigner.Tests/RemoteSigner.Tests.csproj +++ b/RemoteSigner.Tests/RemoteSigner.Tests.csproj @@ -20,4 +20,7 @@ + + + \ No newline at end of file From 66efea59415618bd6389b3177992a2581dd73021 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=CC=81=20A=2EP?= <53834183+Jossec101@users.noreply.github.com> Date: Mon, 5 Oct 2026 12:51:16 +0200 Subject: [PATCH 2/4] Vendor the NSpark fork and build the image from the repository root - vendor/nspark: Elenpay fork of NSpark (native libspark_frost rebuilt from Lightspark source, NSpark.RemoteSigner contract) as a git submodule. - RemoteSigner references NSpark.RemoteSigner; NBitcoin 10.0.9 -> 10.0.10 (NSpark's minimum). The golden PSBT outputs are unchanged. - Dockerfile builds from the repo root (docker build -f RemoteSigner/Dockerfile .), with a root .dockerignore; justfile, lambda tools defaults and CI follow. - CI checks out the submodule. No behavior change: nothing calls into NSpark yet. --- .dockerignore | 13 +++++++++++++ .github/workflows/ci.yml | 7 ++++++- .gitmodules | 4 ++++ RemoteSigner/Dockerfile | 20 ++++++++++---------- RemoteSigner/RemoteSigner.csproj | 10 +++++++++- RemoteSigner/aws-lambda-tools-defaults.json | 1 + justfile | 5 +++-- vendor/nspark | 1 + 8 files changed, 47 insertions(+), 14 deletions(-) create mode 100644 .dockerignore create mode 100644 .gitmodules create mode 160000 vendor/nspark diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..a013dd6 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,13 @@ +# Docker build context is the repository root (see RemoteSigner/Dockerfile). +**/.git +**/.github +**/bin +**/obj +**/.DS_Store +**/*.user +RemoteSigner.Tests/ +RemoteSigner.SeedCeremony/ +vendor/nspark/tests/ +vendor/nspark/samples/ +vendor/nspark/benchmarks/ +vendor/nspark/docs/ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b77949e..b3f7534 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,6 +18,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + submodules: true - name: Setup .NET uses: actions/setup-dotnet@v4 @@ -43,6 +45,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + submodules: true - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -68,7 +72,8 @@ jobs: - name: Build and push uses: docker/build-push-action@v6 with: - context: ./RemoteSigner + context: . + file: RemoteSigner/Dockerfile push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..f969226 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "vendor/nspark"] + path = vendor/nspark + url = https://github.com/Elenpay/nspark.git + branch = main diff --git a/RemoteSigner/Dockerfile b/RemoteSigner/Dockerfile index 5f81a73..21f2361 100644 --- a/RemoteSigner/Dockerfile +++ b/RemoteSigner/Dockerfile @@ -1,22 +1,22 @@ +# Build context: the repository root (the NSpark fork lives in the vendor/nspark submodule). +# docker build -f RemoteSigner/Dockerfile . FROM --platform=linux/amd64 public.ecr.aws/lambda/dotnet:10 AS base -FROM --platform=linux/amd64 mcr.microsoft.com/dotnet/sdk:10.0 as build +FROM --platform=linux/amd64 mcr.microsoft.com/dotnet/sdk:10.0 AS build WORKDIR /src -COPY ["RemoteSigner.csproj", "RemoteSigner/"] -RUN dotnet restore "RemoteSigner/RemoteSigner.csproj" +COPY ["RemoteSigner/RemoteSigner.csproj", "RemoteSigner/"] +COPY ["vendor/nspark/", "vendor/nspark/"] +RUN dotnet restore "RemoteSigner/RemoteSigner.csproj" --runtime linux-x64 -WORKDIR "/src/RemoteSigner" -COPY . . -RUN dotnet build "RemoteSigner.csproj" --configuration Release --output /app/build - -FROM build AS publish -RUN dotnet publish "RemoteSigner.csproj" \ +COPY ["RemoteSigner/", "RemoteSigner/"] +RUN dotnet publish "RemoteSigner/RemoteSigner.csproj" \ --configuration Release \ --runtime linux-x64 \ --self-contained false \ + --no-restore \ --output /app/publish \ -p:PublishReadyToRun=true FROM base AS final WORKDIR /var/task -COPY --from=publish /app/publish . \ No newline at end of file +COPY --from=build /app/publish . diff --git a/RemoteSigner/RemoteSigner.csproj b/RemoteSigner/RemoteSigner.csproj index e443ca2..174a580 100644 --- a/RemoteSigner/RemoteSigner.csproj +++ b/RemoteSigner/RemoteSigner.csproj @@ -18,6 +18,14 @@ - + + + + + + + + \ No newline at end of file diff --git a/RemoteSigner/aws-lambda-tools-defaults.json b/RemoteSigner/aws-lambda-tools-defaults.json index 7c16616..c905ac7 100644 --- a/RemoteSigner/aws-lambda-tools-defaults.json +++ b/RemoteSigner/aws-lambda-tools-defaults.json @@ -23,5 +23,6 @@ "tracing-mode" : "PassThrough", "environment-variables" : "\"MF_ed0210c8\"=\"{\"EncryptedSeedphrase\":\"AQICAHheBtxW\\u002B2iTBvvhvmRXaxaScHh6up1/VWCRSMlopexrdwE1C/ylXBL5pmjJ3P/UG7XnAAABBzCCAQMGCSqGSIb3DQEHBqCB9TCB8gIBADCB7AYJKoZIhvcNAQcBMB4GCWCGSAFlAwQBLjARBAxPlkxPX65p7aRcXykCARCAgb4En2Bb/nWQ6m4i3JDP\\u002BKGjaGDAVF4LR6\\u002B2Ljl7orp6pfZbCCxK6e89OBpJWi7elQM670vD/SWkYSZ9MUWUshU8n7NyBJZuZgBhtaH6j6yDhgHtBv7cwJngv0d72QEaTrH2YqLCVuoddEKEpB13ezfkf56230QD134kcJze4fITQGA6sXxQ0x\\u002BWjKOeYltpB\\u002BShk4\\u002BkaNja42ZM0MMjyrMOmQtXCkgdoTUVi6twiqU\\u002Bqr8mQEEq0aNdZzlLCI/v\",\"AwsKmsKeyId\":\"mrk-cec3e3ef59bc4616a6f44da60bfea0ba\"}\"", "dockerfile" : "Dockerfile", + "docker-build-working-dir" : "..", "image-tag" : "nodeguardremotesigner:latest" } \ No newline at end of file diff --git a/justfile b/justfile index ec32d0b..e7049fb 100755 --- a/justfile +++ b/justfile @@ -2,8 +2,9 @@ set positional-arguments build-docker-image: - # Navigate to the RemoteSigner directory and build the Docker image - cd RemoteSigner && docker build -t 839166930136.dkr.ecr.eu-central-1.amazonaws.com/nodeguardremotesigner:latest . + # Build from the repository root: the image needs the vendor/nspark submodule + git submodule update --init vendor/nspark + docker build -f RemoteSigner/Dockerfile -t 839166930136.dkr.ecr.eu-central-1.amazonaws.com/nodeguardremotesigner:latest . push-docker-image: # Push the Docker image to the repository diff --git a/vendor/nspark b/vendor/nspark new file mode 160000 index 0000000..9c61e74 --- /dev/null +++ b/vendor/nspark @@ -0,0 +1 @@ +Subproject commit 9c61e74844e1798bfe0afa803b3d3b6d7dd1d8ec From eff9e30eeaf4a0943d8984b73bcb30b899bbc49b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=CC=81=20A=2EP?= <53834183+Jossec101@users.noreply.github.com> Date: Fri, 9 Oct 2026 18:55:49 +0200 Subject: [PATCH 3/4] Take NSpark from orklabs/nspark, not a fork The NSpark changes go as pull requests straight to orklabs/nspark, so vendor/nspark points there. The submodule pins the head of that stack until it merges upstream. --- .gitmodules | 2 +- RemoteSigner/Dockerfile | 2 +- RemoteSigner/RemoteSigner.csproj | 2 +- vendor/nspark | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.gitmodules b/.gitmodules index f969226..6c6af50 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,4 @@ [submodule "vendor/nspark"] path = vendor/nspark - url = https://github.com/Elenpay/nspark.git + url = https://github.com/orklabs/nspark.git branch = main diff --git a/RemoteSigner/Dockerfile b/RemoteSigner/Dockerfile index 21f2361..a414e29 100644 --- a/RemoteSigner/Dockerfile +++ b/RemoteSigner/Dockerfile @@ -1,4 +1,4 @@ -# Build context: the repository root (the NSpark fork lives in the vendor/nspark submodule). +# Build context: the repository root (NSpark lives in the vendor/nspark submodule). # docker build -f RemoteSigner/Dockerfile . FROM --platform=linux/amd64 public.ecr.aws/lambda/dotnet:10 AS base diff --git a/RemoteSigner/RemoteSigner.csproj b/RemoteSigner/RemoteSigner.csproj index 174a580..aa0e818 100644 --- a/RemoteSigner/RemoteSigner.csproj +++ b/RemoteSigner/RemoteSigner.csproj @@ -21,7 +21,7 @@ - + diff --git a/vendor/nspark b/vendor/nspark index 9c61e74..07d2ce4 160000 --- a/vendor/nspark +++ b/vendor/nspark @@ -1 +1 @@ -Subproject commit 9c61e74844e1798bfe0afa803b3d3b6d7dd1d8ec +Subproject commit 07d2ce4923c9c6182238bbabb05f01463c7f484d From 704dd5b8ee1839b34169885a19ab3a878de9b160 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jose=CC=81=20A=2EP?= <53834183+Jossec101@users.noreply.github.com> Date: Sat, 10 Oct 2026 01:15:46 +0200 Subject: [PATCH 4/4] Take NSpark from the Elenpay/nspark fork The NSpark changes this signer needs, the RemoteSigner contract among them, are PRs on the Elenpay fork. Pin its stack top until they reach a fork tag. --- .gitmodules | 2 +- RemoteSigner/RemoteSigner.csproj | 2 +- vendor/nspark | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitmodules b/.gitmodules index 6c6af50..f969226 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,4 @@ [submodule "vendor/nspark"] path = vendor/nspark - url = https://github.com/orklabs/nspark.git + url = https://github.com/Elenpay/nspark.git branch = main diff --git a/RemoteSigner/RemoteSigner.csproj b/RemoteSigner/RemoteSigner.csproj index aa0e818..79bf896 100644 --- a/RemoteSigner/RemoteSigner.csproj +++ b/RemoteSigner/RemoteSigner.csproj @@ -21,7 +21,7 @@ - + diff --git a/vendor/nspark b/vendor/nspark index 07d2ce4..5a5fe8c 160000 --- a/vendor/nspark +++ b/vendor/nspark @@ -1 +1 @@ -Subproject commit 07d2ce4923c9c6182238bbabb05f01463c7f484d +Subproject commit 5a5fe8cf231bfa061412efa8c726da714f9e2d88